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
package/lib/core/alchemy.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
let shared_objects
|
|
2
|
-
plugModules
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
let shared_objects = {},
|
|
2
|
+
plugModules = null,
|
|
3
|
+
extraModulePaths = [],
|
|
4
|
+
usedModules = {},
|
|
5
|
+
useErrors = {},
|
|
6
|
+
usePaths = {},
|
|
7
|
+
ac_entries = {},
|
|
7
8
|
parseArgs = require('minimist'),
|
|
8
9
|
libpath = require('path'),
|
|
9
10
|
colors = require('ansi-256-colors'),
|
|
@@ -649,7 +650,7 @@ Alchemy.setMethod(function executeSetting(path) {
|
|
|
649
650
|
*
|
|
650
651
|
* @author Jelle De Loecker <jelle@elevenways.be>
|
|
651
652
|
* @since 0.4.0
|
|
652
|
-
* @version 1.4.
|
|
653
|
+
* @version 1.4.1
|
|
653
654
|
*/
|
|
654
655
|
Alchemy.setMethod(function loadSettings() {
|
|
655
656
|
|
|
@@ -678,18 +679,26 @@ Alchemy.setMethod(function loadSettings() {
|
|
|
678
679
|
let local_path = libpath.resolve(PATH_ROOT, 'app', 'config', 'local'),
|
|
679
680
|
local;
|
|
680
681
|
|
|
681
|
-
// Get the local settings
|
|
682
|
-
|
|
683
|
-
local = require(local_path);
|
|
684
|
-
} catch(err) {
|
|
682
|
+
// Get the local settings (can be skipped for testing)
|
|
683
|
+
if (process.env.ALCHEMY_SKIP_LOCAL_CONFIG === '1') {
|
|
685
684
|
local = {};
|
|
686
|
-
this.setSetting('
|
|
685
|
+
this.setSetting('skipped_local_file', local_path);
|
|
686
|
+
} else {
|
|
687
|
+
try {
|
|
688
|
+
local = require(local_path);
|
|
689
|
+
} catch(err) {
|
|
690
|
+
local = {};
|
|
691
|
+
this.setSetting('no_local_file', local_path);
|
|
692
|
+
}
|
|
687
693
|
}
|
|
688
694
|
|
|
689
695
|
// Default to the environment Protoblast has found
|
|
690
696
|
if (!local.environment) {
|
|
691
697
|
|
|
692
|
-
|
|
698
|
+
// ALCHEMY_ENV takes precedence (useful for testing)
|
|
699
|
+
if (process.env.ALCHEMY_ENV) {
|
|
700
|
+
local.environment = process.env.ALCHEMY_ENV;
|
|
701
|
+
} else if (process.env.ENV) {
|
|
693
702
|
local.environment = Blast.environment;
|
|
694
703
|
} else {
|
|
695
704
|
local.environment = 'dev';
|
|
@@ -1044,6 +1053,32 @@ Alchemy.setMethod(function pathResolve(...path_to_dirs) {
|
|
|
1044
1053
|
}
|
|
1045
1054
|
});
|
|
1046
1055
|
|
|
1056
|
+
/**
|
|
1057
|
+
* Add an additional path for module resolution.
|
|
1058
|
+
* This is useful for test scenarios where PATH_ROOT is a test_root
|
|
1059
|
+
* but modules are installed in the parent plugin's node_modules.
|
|
1060
|
+
*
|
|
1061
|
+
* @author Jelle De Loecker <jelle@elevenways.be>
|
|
1062
|
+
* @since 1.4.1
|
|
1063
|
+
* @version 1.4.1
|
|
1064
|
+
*
|
|
1065
|
+
* @param {string} path The node_modules directory to search
|
|
1066
|
+
*/
|
|
1067
|
+
Alchemy.setMethod(function addModuleSearchPath(path) {
|
|
1068
|
+
|
|
1069
|
+
if (!path || typeof path !== 'string') {
|
|
1070
|
+
return;
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
// Normalize the path
|
|
1074
|
+
path = libpath.resolve(path);
|
|
1075
|
+
|
|
1076
|
+
// Don't add duplicates
|
|
1077
|
+
if (!extraModulePaths.includes(path)) {
|
|
1078
|
+
extraModulePaths.push(path);
|
|
1079
|
+
}
|
|
1080
|
+
});
|
|
1081
|
+
|
|
1047
1082
|
/**
|
|
1048
1083
|
* A wrapper function for requiring modules
|
|
1049
1084
|
*
|
|
@@ -1142,7 +1177,7 @@ Alchemy.setMethod(function use(module_name, register_as, options) {
|
|
|
1142
1177
|
*
|
|
1143
1178
|
* @author Jelle De Loecker <jelle@elevenways.be>
|
|
1144
1179
|
* @since 0.0.1
|
|
1145
|
-
* @version 1.4.
|
|
1180
|
+
* @version 1.4.1
|
|
1146
1181
|
*
|
|
1147
1182
|
* @param {string} startPath The path to originate the search from
|
|
1148
1183
|
* @param {string} moduleName
|
|
@@ -1242,6 +1277,21 @@ Alchemy.setMethod(function searchModule(startPath, moduleName, recurse) {
|
|
|
1242
1277
|
}
|
|
1243
1278
|
}
|
|
1244
1279
|
|
|
1280
|
+
// If still not found, check extra module paths registered via addModuleSearchPath()
|
|
1281
|
+
// This is used by the TestHarness to add the plugin's node_modules directory
|
|
1282
|
+
if (!module_path && extraModulePaths.length > 0) {
|
|
1283
|
+
for (let extra_path of extraModulePaths) {
|
|
1284
|
+
try {
|
|
1285
|
+
module_path = require.resolve(libpath.resolve(extra_path, moduleName));
|
|
1286
|
+
if (module_path) {
|
|
1287
|
+
break;
|
|
1288
|
+
}
|
|
1289
|
+
} catch (err) {
|
|
1290
|
+
// Not found in this path, continue
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1245
1295
|
return module_path;
|
|
1246
1296
|
});
|
|
1247
1297
|
|
|
@@ -1502,12 +1552,33 @@ Alchemy.setMethod(function castObjectId(obj) {
|
|
|
1502
1552
|
*
|
|
1503
1553
|
* @author Jelle De Loecker <jelle@elevenways.be>
|
|
1504
1554
|
* @since 0.2.0
|
|
1505
|
-
* @version 1.
|
|
1555
|
+
* @version 1.4.1
|
|
1506
1556
|
*
|
|
1507
1557
|
* @return {boolean}
|
|
1508
1558
|
*/
|
|
1509
1559
|
Alchemy.setMethod(function isStream(obj) {
|
|
1510
|
-
|
|
1560
|
+
|
|
1561
|
+
if (!obj) {
|
|
1562
|
+
return false;
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
// Check for Node.js Stream instance (includes HTTP responses)
|
|
1566
|
+
// Classes.Stream.Stream is provided by Protoblast (see stream_ns.js)
|
|
1567
|
+
if (obj instanceof Classes.Stream.Stream) {
|
|
1568
|
+
return true;
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
// Fallback to duck-typing for internal stream methods
|
|
1572
|
+
if ((typeof obj._read == 'function' || typeof obj._write == 'function') && typeof obj.on === 'function') {
|
|
1573
|
+
return true;
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1576
|
+
// Additional duck-typing check for writable-like objects
|
|
1577
|
+
if (typeof obj.write == 'function' && typeof obj.end == 'function' && typeof obj.on == 'function') {
|
|
1578
|
+
return true;
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1581
|
+
return false;
|
|
1511
1582
|
});
|
|
1512
1583
|
|
|
1513
1584
|
/**
|
|
@@ -2099,7 +2170,7 @@ Alchemy.setMethod(function createExportStream(options) {
|
|
|
2099
2170
|
*
|
|
2100
2171
|
* @author Jelle De Loecker <jelle@elevenways.be>
|
|
2101
2172
|
* @since 1.0.5
|
|
2102
|
-
* @version 1.
|
|
2173
|
+
* @version 1.4.1
|
|
2103
2174
|
*
|
|
2104
2175
|
* @param {Stream} output
|
|
2105
2176
|
* @param {Object} options
|
|
@@ -2126,10 +2197,16 @@ Alchemy.setMethod(function exportToStream(output, options) {
|
|
|
2126
2197
|
}
|
|
2127
2198
|
|
|
2128
2199
|
let tasks = [],
|
|
2200
|
+
models = Model.getAllChildren(),
|
|
2129
2201
|
i;
|
|
2130
2202
|
|
|
2131
|
-
for (i = 0; i <
|
|
2132
|
-
let model =
|
|
2203
|
+
for (i = 0; i < models.length; i++) {
|
|
2204
|
+
let model = models[i];
|
|
2205
|
+
|
|
2206
|
+
// Skip abstract models - they have no records
|
|
2207
|
+
if (model.is_abstract) {
|
|
2208
|
+
continue;
|
|
2209
|
+
}
|
|
2133
2210
|
|
|
2134
2211
|
tasks.push(async function exportModel(next) {
|
|
2135
2212
|
await (new model).exportToStream(output);
|
|
@@ -2152,7 +2229,7 @@ Alchemy.setMethod(function exportToStream(output, options) {
|
|
|
2152
2229
|
*
|
|
2153
2230
|
* @author Jelle De Loecker <jelle@elevenways.be>
|
|
2154
2231
|
* @since 1.0.5
|
|
2155
|
-
* @version 1.
|
|
2232
|
+
* @version 1.4.1
|
|
2156
2233
|
*
|
|
2157
2234
|
* @param {Stream} input
|
|
2158
2235
|
* @param {Object} options
|
|
@@ -2178,155 +2255,26 @@ Alchemy.setMethod(function importFromStream(input, options) {
|
|
|
2178
2255
|
options = {};
|
|
2179
2256
|
}
|
|
2180
2257
|
|
|
2181
|
-
let
|
|
2182
|
-
current_type = null,
|
|
2183
|
-
extra_stream,
|
|
2184
|
-
pledge = new Pledge(),
|
|
2185
|
-
stopped,
|
|
2186
|
-
paused,
|
|
2187
|
-
buffer,
|
|
2188
|
-
model,
|
|
2189
|
-
value,
|
|
2190
|
-
seen = 0,
|
|
2191
|
-
left,
|
|
2192
|
-
size,
|
|
2193
|
-
doc;
|
|
2194
|
-
|
|
2195
|
-
input.on('data', function onData(data) {
|
|
2196
|
-
|
|
2197
|
-
if (stopped) {
|
|
2198
|
-
return;
|
|
2199
|
-
}
|
|
2200
|
-
|
|
2201
|
-
if (buffer) {
|
|
2202
|
-
buffer = Buffer.concat([buffer, data]);
|
|
2203
|
-
} else {
|
|
2204
|
-
buffer = data;
|
|
2205
|
-
}
|
|
2206
|
-
|
|
2207
|
-
handleBuffer();
|
|
2208
|
-
});
|
|
2209
|
-
|
|
2210
|
-
function handleBuffer() {
|
|
2211
|
-
|
|
2212
|
-
if (paused) {
|
|
2213
|
-
return;
|
|
2214
|
-
}
|
|
2215
|
-
|
|
2216
|
-
if (!current_type && buffer.length < 2) {
|
|
2217
|
-
return;
|
|
2218
|
-
}
|
|
2219
|
-
|
|
2220
|
-
if (!current_type) {
|
|
2221
|
-
current_type = buffer.readUInt8(0);
|
|
2222
|
-
|
|
2223
|
-
if (current_type == 0x01) {
|
|
2224
|
-
size = buffer.readUInt8(1);
|
|
2225
|
-
buffer = buffer.slice(2);
|
|
2226
|
-
} else if (current_type == 0x02 && buffer.length >= 5) {
|
|
2227
|
-
size = buffer.readUInt32BE(1);
|
|
2228
|
-
buffer = buffer.slice(5);
|
|
2229
|
-
} else if (current_type == 0xFF) {
|
|
2230
|
-
size = buffer.readUInt32BE(1);
|
|
2231
|
-
buffer = buffer.slice(5);
|
|
2232
|
-
seen = 0;
|
|
2233
|
-
|
|
2234
|
-
if (!doc) {
|
|
2235
|
-
stopped = true;
|
|
2236
|
-
pledge.reject(new Error('Found extra import data, but no active document'));
|
|
2237
|
-
} else {
|
|
2238
|
-
extra_stream = new require('stream').PassThrough();
|
|
2239
|
-
doc.extraImportFromStream(extra_stream);
|
|
2240
|
-
}
|
|
2241
|
-
} else {
|
|
2242
|
-
// Not enough data? Wait
|
|
2243
|
-
current_type = null;
|
|
2244
|
-
return;
|
|
2245
|
-
}
|
|
2246
|
-
}
|
|
2247
|
-
|
|
2248
|
-
handleRest();
|
|
2249
|
-
}
|
|
2250
|
-
|
|
2251
|
-
function handleRest() {
|
|
2252
|
-
|
|
2253
|
-
if (current_type == 0xFF) {
|
|
2254
|
-
left = size - seen;
|
|
2255
|
-
value = buffer.slice(0, left);
|
|
2256
|
-
|
|
2257
|
-
seen += value.length;
|
|
2258
|
-
|
|
2259
|
-
if (value.length == buffer.length) {
|
|
2260
|
-
buffer = null;
|
|
2261
|
-
} else if (value.length < buffer.length) {
|
|
2262
|
-
buffer = buffer.slice(left);
|
|
2263
|
-
}
|
|
2264
|
-
|
|
2265
|
-
extra_stream.write(value);
|
|
2266
|
-
|
|
2267
|
-
if (value.length == left) {
|
|
2268
|
-
extra_stream.end();
|
|
2269
|
-
current_type = null;
|
|
2270
|
-
|
|
2271
|
-
if (buffer) {
|
|
2272
|
-
handleBuffer();
|
|
2273
|
-
}
|
|
2274
|
-
}
|
|
2275
|
-
|
|
2276
|
-
return;
|
|
2277
|
-
}
|
|
2258
|
+
let parser = new Classes.Alchemy.ImportStreamParser(input, options);
|
|
2278
2259
|
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
return;
|
|
2260
|
+
// Model resolver for full database import:
|
|
2261
|
+
// Switches between models as they appear in the stream
|
|
2262
|
+
parser.setModelResolver(function resolveModel(model_name, current_model) {
|
|
2263
|
+
// Reuse current model if name matches
|
|
2264
|
+
if (current_model && current_model.model_name == model_name) {
|
|
2265
|
+
return current_model;
|
|
2285
2266
|
}
|
|
2286
2267
|
|
|
2287
|
-
|
|
2288
|
-
value = value.toString();
|
|
2289
|
-
|
|
2290
|
-
if (!model || model.model_name != value) {
|
|
2291
|
-
model = Model.get(value);
|
|
2292
|
-
doc = null;
|
|
2293
|
-
}
|
|
2294
|
-
|
|
2295
|
-
if (!model) {
|
|
2296
|
-
stopped = true;
|
|
2297
|
-
return pledge.reject(new Error('Could not find Model "' + value + '"'));
|
|
2298
|
-
}
|
|
2268
|
+
let model = Model.get(model_name);
|
|
2299
2269
|
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
} else if (current_type == 0x02) {
|
|
2303
|
-
doc = model.createDocument();
|
|
2304
|
-
input.pause();
|
|
2305
|
-
paused = true;
|
|
2306
|
-
|
|
2307
|
-
doc.importFromBuffer(value).done(function done(err, result) {
|
|
2308
|
-
|
|
2309
|
-
if (err) {
|
|
2310
|
-
stopped = true;
|
|
2311
|
-
return pledge.reject(err);
|
|
2312
|
-
}
|
|
2313
|
-
|
|
2314
|
-
current_type = null;
|
|
2315
|
-
paused = false;
|
|
2316
|
-
input.resume();
|
|
2317
|
-
|
|
2318
|
-
handleBuffer();
|
|
2319
|
-
});
|
|
2320
|
-
|
|
2321
|
-
return;
|
|
2270
|
+
if (!model) {
|
|
2271
|
+
throw new Error('Could not find Model "' + model_name + '"');
|
|
2322
2272
|
}
|
|
2323
2273
|
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
}
|
|
2327
|
-
}
|
|
2274
|
+
return model;
|
|
2275
|
+
});
|
|
2328
2276
|
|
|
2329
|
-
return
|
|
2277
|
+
return parser.parse();
|
|
2330
2278
|
});
|
|
2331
2279
|
|
|
2332
2280
|
/**
|
|
@@ -538,9 +538,55 @@ Alchemy.setMethod(function addViewDirectory(dirPath, weight) {
|
|
|
538
538
|
* This immediately executes the plugin's bootstrap.js file,
|
|
539
539
|
* but the loading of the app tree happens later.
|
|
540
540
|
*
|
|
541
|
+
/**
|
|
542
|
+
* Handle a fatal plugin error
|
|
543
|
+
* Plugin errors are fatal - the server cannot start with a broken plugin
|
|
544
|
+
*
|
|
545
|
+
* @author Jelle De Loecker <jelle@elevenways.be>
|
|
546
|
+
* @since 1.4.1
|
|
547
|
+
* @version 1.4.1
|
|
548
|
+
*
|
|
549
|
+
* @param {string} plugin_name The name of the plugin
|
|
550
|
+
* @param {string} phase Which phase failed (not_found, settings, bootstrap, start)
|
|
551
|
+
* @param {Error} err The error that occurred
|
|
552
|
+
* @param {Object} extra_info Additional info for error tracking
|
|
553
|
+
*/
|
|
554
|
+
Alchemy.setMethod(function handlePluginError(plugin_name, phase, err, extra_info) {
|
|
555
|
+
|
|
556
|
+
let message = 'Failed to load plugin "' + plugin_name + '"';
|
|
557
|
+
|
|
558
|
+
if (phase) {
|
|
559
|
+
message += ' during ' + phase + ' phase';
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// Log the full error
|
|
563
|
+
log.error(message);
|
|
564
|
+
log.error('Error:', err.message);
|
|
565
|
+
|
|
566
|
+
if (err.stack) {
|
|
567
|
+
log.error('Stack:', err.stack);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// Register with error tracking services (Sentry, Glitchtip, etc.)
|
|
571
|
+
let error_info = Object.assign({
|
|
572
|
+
context : message,
|
|
573
|
+
plugin : plugin_name,
|
|
574
|
+
phase : phase,
|
|
575
|
+
fatal : true,
|
|
576
|
+
}, extra_info);
|
|
577
|
+
|
|
578
|
+
this.registerError(err, error_info);
|
|
579
|
+
|
|
580
|
+
// Die with a clear message
|
|
581
|
+
die('Plugin "' + plugin_name + '" failed to load: ' + err.message, {level: 2});
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
/**
|
|
585
|
+
* Load a plugin
|
|
586
|
+
*
|
|
541
587
|
* @author Jelle De Loecker <jelle@elevenways.be>
|
|
542
588
|
* @since 0.0.1
|
|
543
|
-
* @version 1.4.
|
|
589
|
+
* @version 1.4.1
|
|
544
590
|
*
|
|
545
591
|
* @param {string} name The name of the plugin (which is its path)
|
|
546
592
|
* @param {Object} options Options to pass to the plugin
|
|
@@ -604,7 +650,10 @@ Alchemy.setMethod(function usePlugin(name, options) {
|
|
|
604
650
|
}
|
|
605
651
|
|
|
606
652
|
if (!is_dir) {
|
|
607
|
-
|
|
653
|
+
let err = new Error('Plugin directory not found. Searched: ' + possible_paths.join(', '));
|
|
654
|
+
err.searched_paths = possible_paths;
|
|
655
|
+
|
|
656
|
+
alchemy.handlePluginError(name, 'not_found', err, {paths: possible_paths});
|
|
608
657
|
return false;
|
|
609
658
|
}
|
|
610
659
|
|
|
@@ -613,7 +662,15 @@ Alchemy.setMethod(function usePlugin(name, options) {
|
|
|
613
662
|
// Set the given options
|
|
614
663
|
alchemy.plugins[name] = instance;
|
|
615
664
|
|
|
616
|
-
|
|
665
|
+
// doPreload can fail - if it does, the plugin will call die()
|
|
666
|
+
// but we still need to clean up if for some reason it doesn't
|
|
667
|
+
let result = instance.doPreload();
|
|
668
|
+
|
|
669
|
+
if (result === false) {
|
|
670
|
+
// Plugin failed to load - remove it from plugins
|
|
671
|
+
delete alchemy.plugins[name];
|
|
672
|
+
return false;
|
|
673
|
+
}
|
|
617
674
|
|
|
618
675
|
return instance;
|
|
619
676
|
});
|
|
@@ -623,7 +680,7 @@ Alchemy.setMethod(function usePlugin(name, options) {
|
|
|
623
680
|
*
|
|
624
681
|
* @author Jelle De Loecker <jelle@elevenways.be>
|
|
625
682
|
* @since 0.0.1
|
|
626
|
-
* @version 1.4.
|
|
683
|
+
* @version 1.4.1
|
|
627
684
|
*
|
|
628
685
|
* @param {string|Array} names
|
|
629
686
|
* @param {boolean} attempt_require
|
|
@@ -659,7 +716,9 @@ Alchemy.setMethod(function requirePlugin(names, attempt_require) {
|
|
|
659
716
|
if (!plugin_stage || plugin_stage.started) {
|
|
660
717
|
// If the plugin stage has already started,
|
|
661
718
|
// manually start this plugin now
|
|
662
|
-
|
|
719
|
+
if (temp.startPlugin) {
|
|
720
|
+
temp.startPlugin();
|
|
721
|
+
}
|
|
663
722
|
}
|
|
664
723
|
continue;
|
|
665
724
|
}
|
package/lib/core/base.js
CHANGED
|
@@ -391,7 +391,7 @@ Base.setStatic('starts_new_group', false);
|
|
|
391
391
|
*
|
|
392
392
|
* @author Jelle De Loecker <jelle@elevenways.be>
|
|
393
393
|
* @since 1.1.8
|
|
394
|
-
* @version 1.
|
|
394
|
+
* @version 1.4.1
|
|
395
395
|
*
|
|
396
396
|
* @type {Conduit}
|
|
397
397
|
*/
|
|
@@ -414,7 +414,7 @@ Base.setProperty(function conduit() {
|
|
|
414
414
|
result = renderer.root_renderer.conduit;
|
|
415
415
|
}
|
|
416
416
|
|
|
417
|
-
if (!
|
|
417
|
+
if (!result && renderer?.server_var) {
|
|
418
418
|
result = renderer.server_var('conduit');
|
|
419
419
|
}
|
|
420
420
|
|
package/lib/core/middleware.js
CHANGED
|
@@ -1207,18 +1207,44 @@ async function createAlchemyScss() {
|
|
|
1207
1207
|
*
|
|
1208
1208
|
* @author Jelle De Loecker <jelle@elevenways.be>
|
|
1209
1209
|
* @since 1.4.0
|
|
1210
|
-
* @version 1.4.
|
|
1210
|
+
* @version 1.4.1
|
|
1211
1211
|
*
|
|
1212
1212
|
* @param {string} path_to_import The requested path to import
|
|
1213
1213
|
* @param {string} current_file_path The path to the current file
|
|
1214
1214
|
*/
|
|
1215
|
-
|
|
1215
|
+
/**
|
|
1216
|
+
* Create a custom SCSS importer for a specific main file
|
|
1217
|
+
*
|
|
1218
|
+
* @param {string} main_file_path The path to the main SCSS file being compiled
|
|
1219
|
+
*/
|
|
1220
|
+
function createScssImporter(main_file_path) {
|
|
1221
|
+
return async function customScssImporter(path_to_import, current_file_path) {
|
|
1222
|
+
return resolveScssImport(path_to_import, current_file_path, main_file_path);
|
|
1223
|
+
};
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
/**
|
|
1227
|
+
* Resolve an SCSS import path
|
|
1228
|
+
*
|
|
1229
|
+
* @param {string} path_to_import The requested path to import
|
|
1230
|
+
* @param {string} current_file_path The path to the file doing the import (may be 'stdin')
|
|
1231
|
+
* @param {string} main_file_path The path to the main SCSS file being compiled
|
|
1232
|
+
*/
|
|
1233
|
+
async function resolveScssImport(path_to_import, current_file_path, main_file_path) {
|
|
1216
1234
|
|
|
1217
1235
|
// The alchemy.scss file is a special case
|
|
1218
1236
|
if (path_to_import === 'alchemy.scss' || path_to_import === 'alchemy') {
|
|
1219
1237
|
return createAlchemyScss();
|
|
1220
1238
|
}
|
|
1221
1239
|
|
|
1240
|
+
// SASS may pass non-path values for current_file_path:
|
|
1241
|
+
// - 'stdin' when processing the main file
|
|
1242
|
+
// - A simple name like 'alchemy' when processing imports from {contents: ...} returns
|
|
1243
|
+
// In these cases, fall back to main_file_path
|
|
1244
|
+
if (main_file_path && (!current_file_path || !libpath.isAbsolute(current_file_path))) {
|
|
1245
|
+
current_file_path = main_file_path;
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1222
1248
|
// Get the current directory this SCSS file is in
|
|
1223
1249
|
let current_dir = libpath.dirname(current_file_path);
|
|
1224
1250
|
|
|
@@ -1271,7 +1297,7 @@ async function customScssImporter(path_to_import, current_file_path) {
|
|
|
1271
1297
|
// Look for files starting with an underscore
|
|
1272
1298
|
if (filename_to_import[0] != '_') {
|
|
1273
1299
|
let dashed_filename_to_import = '_' + filename_to_import;
|
|
1274
|
-
return
|
|
1300
|
+
return resolveScssImport(libpath.join(dir_to_import, dashed_filename_to_import), current_file_path, main_file_path);
|
|
1275
1301
|
}
|
|
1276
1302
|
|
|
1277
1303
|
let extension = libpath.extname(filename_to_import);
|
|
@@ -1279,7 +1305,7 @@ async function customScssImporter(path_to_import, current_file_path) {
|
|
|
1279
1305
|
// If no extension was given, look for that too
|
|
1280
1306
|
if (!extension) {
|
|
1281
1307
|
filename_to_import += '.scss';
|
|
1282
|
-
return
|
|
1308
|
+
return resolveScssImport(libpath.join(dir_to_import, filename_to_import), current_file_path, main_file_path);
|
|
1283
1309
|
}
|
|
1284
1310
|
|
|
1285
1311
|
if (path_to_import.startsWith('/overrides/')) {
|
|
@@ -1356,7 +1382,7 @@ Alchemy.setMethod(function getCompiledSassPath(sassPath, options, callback) {
|
|
|
1356
1382
|
silenceDeprecations: ['legacy-js-api'],
|
|
1357
1383
|
includePaths : styleDirs.getSorted(),
|
|
1358
1384
|
functions : custom_functions,
|
|
1359
|
-
importer :
|
|
1385
|
+
importer : createScssImporter(sassPath),
|
|
1360
1386
|
logger : {
|
|
1361
1387
|
warn : logSassWarning,
|
|
1362
1388
|
debug : logSassDebug,
|
package/lib/core/prefix.js
CHANGED
|
@@ -242,7 +242,7 @@ Connection.url = function url(connectionName, options) {
|
|
|
242
242
|
Connection.fill = function fill(url, params) {
|
|
243
243
|
|
|
244
244
|
if (params) {
|
|
245
|
-
for (paramName in params) {
|
|
245
|
+
for (let paramName in params) {
|
|
246
246
|
url = url.replace(':'+paramName, params[paramName]);
|
|
247
247
|
}
|
|
248
248
|
}
|
package/lib/core/setting.js
CHANGED
|
@@ -8,7 +8,7 @@ const VALUE = Symbol('value');
|
|
|
8
8
|
*
|
|
9
9
|
* @author Jelle De Loecker <jelle@elevenways.be>
|
|
10
10
|
* @since 1.4.0
|
|
11
|
-
* @version 1.4.
|
|
11
|
+
* @version 1.4.1
|
|
12
12
|
*
|
|
13
13
|
* @param {string} name The name of the setting in its group
|
|
14
14
|
* @param {Object} config The settings of this definition
|
|
@@ -32,7 +32,7 @@ const Base = Function.inherits('Alchemy.Base', 'Alchemy.Setting', function Base(
|
|
|
32
32
|
this.view_permission = config?.view_permission;
|
|
33
33
|
|
|
34
34
|
// Does this setting require any permission to edit?
|
|
35
|
-
this.
|
|
35
|
+
this.edit_permission = config?.edit_permission;
|
|
36
36
|
});
|
|
37
37
|
|
|
38
38
|
/**
|
|
@@ -317,7 +317,7 @@ Definition.setMethod(function toJSON() {
|
|
|
317
317
|
show_description : this.show_description,
|
|
318
318
|
target : this.target,
|
|
319
319
|
view_permission : this.view_permission,
|
|
320
|
-
|
|
320
|
+
edit_permission : this.edit_permission,
|
|
321
321
|
requires_restart : this.requires_restart,
|
|
322
322
|
locked : this.locked,
|
|
323
323
|
};
|
|
@@ -366,7 +366,7 @@ Definition.setMethod(function getEditorConfiguration(root_value, editor_context)
|
|
|
366
366
|
result.description = this.description;
|
|
367
367
|
}
|
|
368
368
|
|
|
369
|
-
if (this.
|
|
369
|
+
if (this.edit_permission && editor_context && !editor_context.hasPermission(this.edit_permission)) {
|
|
370
370
|
result.locked = true;
|
|
371
371
|
}
|
|
372
372
|
|
|
@@ -413,7 +413,7 @@ Definition.setMethod(function canBeEditedBy(permission_context) {
|
|
|
413
413
|
}
|
|
414
414
|
|
|
415
415
|
// If no edit permission is required, it can be edited
|
|
416
|
-
if (!this.
|
|
416
|
+
if (!this.edit_permission) {
|
|
417
417
|
return true;
|
|
418
418
|
}
|
|
419
419
|
|
|
@@ -422,7 +422,7 @@ Definition.setMethod(function canBeEditedBy(permission_context) {
|
|
|
422
422
|
return false;
|
|
423
423
|
}
|
|
424
424
|
|
|
425
|
-
return permission_context.hasPermission(this.
|
|
425
|
+
return permission_context.hasPermission(this.edit_permission);
|
|
426
426
|
});
|
|
427
427
|
|
|
428
428
|
/**
|
|
@@ -539,7 +539,7 @@ Group.setMethod(function toDry() {
|
|
|
539
539
|
setting_id : this.setting_id,
|
|
540
540
|
description : this.description,
|
|
541
541
|
view_permission : this.view_permission,
|
|
542
|
-
|
|
542
|
+
edit_permission : this.edit_permission,
|
|
543
543
|
};
|
|
544
544
|
|
|
545
545
|
let children = [...this.children.values()];
|
|
@@ -565,7 +565,7 @@ Group.setMethod(function toEnumEntry() {
|
|
|
565
565
|
setting_id : this.setting_id,
|
|
566
566
|
description : this.description,
|
|
567
567
|
view_permission : this.view_permission,
|
|
568
|
-
|
|
568
|
+
edit_permission : this.edit_permission,
|
|
569
569
|
is_group : true,
|
|
570
570
|
};
|
|
571
571
|
});
|
|
@@ -868,7 +868,10 @@ Group.setMethod(function assign(target, values, default_only, do_actions = true)
|
|
|
868
868
|
|
|
869
869
|
// Make sure it's correct
|
|
870
870
|
if (group) {
|
|
871
|
-
|
|
871
|
+
// We need a GroupValue. If target[key] doesn't exist, isn't an object,
|
|
872
|
+
// or is a non-group Value (e.g., SettingValue from config), generate a new one.
|
|
873
|
+
// This handles the case where config sets `key: null` before bootstrap creates the group.
|
|
874
|
+
if (!target[key] || typeof target[key] !== 'object' || (target[key] instanceof Value && !target[key].is_group)) {
|
|
872
875
|
target[key] = group.generateValue();
|
|
873
876
|
}
|
|
874
877
|
|
|
@@ -115,5 +115,9 @@ DEFINE('die', function die(...args) {
|
|
|
115
115
|
// (but blessed can't revert to original state without segfaulting)
|
|
116
116
|
alchemy.Janeway.print(alchemy.SEVERE, args, {level: 2});
|
|
117
117
|
|
|
118
|
-
|
|
118
|
+
// Give logs time to flush before exiting
|
|
119
|
+
Blast.sleepSync(1000);
|
|
120
|
+
|
|
121
|
+
// Exit with error code 1 to indicate failure
|
|
122
|
+
process.exit(1);
|
|
119
123
|
});
|
|
@@ -341,11 +341,17 @@ const app_bootstrap = load_core.createStage('app_bootstrap', () => {
|
|
|
341
341
|
try {
|
|
342
342
|
alchemy.useOnce(libpath.resolve(PATH_ROOT, 'app', 'config', 'bootstrap'));
|
|
343
343
|
} catch (err) {
|
|
344
|
-
|
|
344
|
+
|
|
345
|
+
// Check if this is a "file not found" error (optional file)
|
|
346
|
+
let is_not_found = err.code === 'ENOENT' || err.message.indexOf('Cannot find') > -1;
|
|
347
|
+
|
|
348
|
+
if (is_not_found) {
|
|
349
|
+
// Bootstrap file is optional - just log a warning
|
|
345
350
|
alchemy.printLog(alchemy.WARNING, 'Could not load app bootstrap file');
|
|
346
|
-
throw err;
|
|
347
351
|
} else {
|
|
352
|
+
// Actual error in the bootstrap file - log and throw
|
|
348
353
|
alchemy.printLog(alchemy.SEVERE, 'Could not load config bootstrap file', {err: err});
|
|
354
|
+
throw err;
|
|
349
355
|
}
|
|
350
356
|
}
|
|
351
357
|
});
|