From bd125f815066860e9996f4177a61945ba7ac3386 Mon Sep 17 00:00:00 2001 From: Jelle De Loecker Date: Thu, 18 Jan 2024 12:40:16 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Add=20setting=20actions=20&=20depen?= =?UTF-8?q?dencies?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/app/model/alchemy_setting_model.js | 83 ++- lib/core/alchemy.js | 80 ++- lib/core/setting.js | 683 ++++++++++++++++++++++--- lib/scripts/create_settings.js | 98 ++++ test/00-init.js | 23 +- 5 files changed, 830 insertions(+), 137 deletions(-) diff --git a/lib/app/model/alchemy_setting_model.js b/lib/app/model/alchemy_setting_model.js index a234124c..bdc4437a 100644 --- a/lib/app/model/alchemy_setting_model.js +++ b/lib/app/model/alchemy_setting_model.js @@ -55,6 +55,57 @@ AlchemySetting.constitute(function chimeraConfig() { edit.addField('configuration') }); +/** + * Apply the given changes + * + * @author Jelle De Loecker + * @since 1.4.0 + * @version 1.4.0 + * + * @param {Object} changes + * @param {Conduit} permission_context + */ +AlchemySetting.setMethod(async function saveChanges(changes, permission_context) { + + if (!changes) { + return; + } + + let setting_id, + new_value, + setting, + doc; + + for (setting_id in changes) { + new_value = changes[setting_id]; + setting = Classes.Alchemy.Setting.SYSTEM.get(setting_id); + + if (!setting) { + throw new Error('Unknown setting: ' + setting_id); + } + + // We don't throw an error here: + // we assume the user should not have seen this setting anyway + if (!setting.canBeEditedBy(permission_context)) { + continue; + } + + doc = await this.findByValues({ + setting_id: setting_id + }); + + if (!doc) { + doc = this.createDocument({ + setting_id + }); + } + + doc.configuration = setting.createConfigurationObject(new_value); + + await doc.save(); + } +}); + /** * Do something before saving the record * @@ -65,19 +116,39 @@ AlchemySetting.constitute(function chimeraConfig() { * @param {Document.AlchemyTask} doc */ AlchemySetting.setMethod(function beforeSave(doc) { - + return doc.applySetting(); }); /** - * Update the schedules after saving + * Apply this setting * * @author Jelle De Loecker * @since 1.4.0 * @version 1.4.0 * - * @param {Object} main - * @param {Object} info + * @param {boolean} do_actions Should the setting actions be executed */ -AlchemySetting.setMethod(function afterSave(main, info) { +AlchemySetting.setDocumentMethod(function applySetting(do_actions = true) { -}); + if (!this.setting_id) { + return; + } + + let setting = Classes.Alchemy.Setting.SYSTEM.get(this.setting_id); + + if (!setting) { + return; + } + + let existing_value = alchemy.system_settings.getPath(this.setting_id); + + if (!existing_value) { + existing_value = setting.generateValue(); + } + + if (do_actions) { + return existing_value.setValue(this.configuration.value); + } else { + return existing_value.setValueSilently(this.configuration.value); + } +}); \ No newline at end of file diff --git a/lib/core/alchemy.js b/lib/core/alchemy.js index 07a5ee05..ec69c9ab 100644 --- a/lib/core/alchemy.js +++ b/lib/core/alchemy.js @@ -467,7 +467,7 @@ Alchemy.setMethod(function isProcessRunning(pid) { * * @author Jelle De Loecker * @since 0.5.0 - * @version 1.3.1 + * @version 1.4.0 * * @param {Object} options */ @@ -527,41 +527,6 @@ Alchemy.setMethod(function startJaneway(options) { this.Janeway.started = true; this.Janeway.start(options); - - if (this.settings.frontend.title) { - let title = this.settings.frontend.title; - this.Janeway.setTitle(title); - } - - if (this.settings.sessions.janeway_menu) { - - let session_menu = this.Janeway.addIndicator('⌨ '); - - if (!session_menu.addItem) { - return session_menu.remove(); - } - - session_menu.addItem({ - title : 'Current active browser sessions:', - weight : Infinity, - }, () => { - console.log('All sessions:', this.sessions); - }); - - this.Janeway.session_menu = session_menu; - } - - if (this.settings.performance.janeway_lag_menu) { - let lag_menu = this.Janeway.addIndicator('0 ms'); - - setInterval(() => { - lag_menu.setIcon(this.lagInMs() + ' ms'); - this.Janeway.redraw(); - }, 900).unref(); - - this.Janeway.lag_menu = lag_menu; - } - }); /** @@ -649,13 +614,44 @@ Alchemy.setMethod(function setSetting(path, value) { */ Alchemy.setMethod(function getSetting(path) { - let value = this.system_settings.getPath(path); + let value = this.getSettingValueInstance(path); if (value) { return value.get(); } }); +/** + * Get a setting value instance + * + * @author Jelle De Loecker + * @since 1.4.0 + * @version 1.4.0 + * + * @param {string} path The path of the setting (without system prefix) + */ +Alchemy.setMethod(function getSettingValueInstance(path) { + return this.system_settings.getPath(path); +}); + +/** + * Execute the action linked to a setting + * + * @author Jelle De Loecker + * @since 1.4.0 + * @version 1.4.0 + * + * @param {string} path The path of the setting (without system prefix) + */ +Alchemy.setMethod(function executeSetting(path) { + + let value = this.getSettingValueInstance(path); + + if (value) { + return value.executeAction(); + } +}); + /** * Load the initial hard-coded settings * @@ -683,7 +679,7 @@ Alchemy.setMethod(function loadSettings() { let value = require(default_path); system.setDefaultValue(value); } catch (err) { - this.setSetting('no_default_file', true); + this.setSetting('no_default_file', default_path); } // Generate the path to the local settings file @@ -695,7 +691,7 @@ Alchemy.setMethod(function loadSettings() { local = require(local_path); } catch(err) { local = {}; - this.setSetting('no_local_file', true); + this.setSetting('no_local_file', local_path); } // Default to the 'dev' environment @@ -721,13 +717,13 @@ Alchemy.setMethod(function loadSettings() { // Get the config try { let value = require(env_path); - system.setValue(value); + system.setValueSilently(value); } catch(err) { - this.setSetting('no_env_file', true); + this.setSetting('no_env_file', env_path); } // And now overlay the final local settings - system.setValue(local); + system.setValueSilently(local); if (!settings.name) { this.setSetting('name', this.package.name); diff --git a/lib/core/setting.js b/lib/core/setting.js index 9ffab244..f0f82c38 100644 --- a/lib/core/setting.js +++ b/lib/core/setting.js @@ -11,9 +11,10 @@ const VALUE = Symbol('value'); * @version 1.4.0 * * @param {string} name The name of the setting in its group + * @param {Object} config The settings of this definition * @param {Group} group The parent group */ -const Base = Function.inherits('Alchemy.Base', 'Alchemy.Setting', function Base(name, group) { +const Base = Function.inherits('Alchemy.Base', 'Alchemy.Setting', function Base(name, config, group) { // The parent group this.group = group; @@ -24,9 +25,14 @@ const Base = Function.inherits('Alchemy.Base', 'Alchemy.Setting', function Base( // The complete setting id this.setting_id = (group?.setting_id ? group.setting_id + '.' : '') + name; - if (this.setting_id === 'system.debugging.logging.definition') { - throw new Error('WRONG') - } + // The description + this.description = config?.description; + + // Does this setting require any permission to view? + this.view_permission = config?.view_permission; + + // Does this setting require any permission to edit? + this.edit_persmission = config?.edit_persmission; }); /** @@ -53,7 +59,7 @@ Base.setProperty('is_group', false); */ const Definition = Function.inherits('Alchemy.Setting.Base', function Definition(name, config, group) { - Definition.super.call(this, name, group); + Definition.super.call(this, name, config, group); // The type of the definition (string, number, etc) this.type = config.type; @@ -67,18 +73,65 @@ const Definition = Function.inherits('Alchemy.Setting.Base', function Definition // The default value this.default_value = config.default; - // The description - this.description = config.description; - // Is the default value an object? this.default_value_needs_cloning = this.default_value && typeof this.default_value == 'object'; // Show a description? this.show_description = config.show_description; + // Is this setting locked? + // (Meaning: can not be edited in the frontend) + this.locked = config.locked; + + // Does this setting require a reboot? + this.requires_restart = config.requires_restart; + // The intended target of this setting. // This is mostly used to differentiate between 'user' or 'visitor' settings this.target = config.target; + + // The action to execute + this.action = config.action; + + // Other settings it might require (needs to be truthy) + this.requires = config.requires ? Array.cast(config.requires) : undefined; + + // Other settings it might depend on + this.depends_on = config.depends_on ? Array.cast(config.depends_on) : undefined; +}); + +/** + * Get all the dependency ids + * + * @author Jelle De Loecker + * @since 1.4.0 + * @version 1.4.0 + * + * @return {string[]} + */ +Definition.setProperty(function all_dependencies() { + + let result = []; + + if (this.depends_on) { + if (Array.isArray(this.depends_on)) { + result.push(...this.depends_on); + } else { + result.push(this.depends_on); + } + } + + if (this.requires) { + let requires = Array.cast(this.requires); + + for (let entry of requires) { + if (result.indexOf(entry) == -1) { + result.push(entry); + } + } + } + + return result; }); /** @@ -171,7 +224,7 @@ Definition.setMethod(function cast(value) { */ Definition.setMethod(function generateValue() { - let result = new Value(this); + let result = new SettingValue(this); result.setDefaultValue(this.default_value); @@ -190,11 +243,16 @@ Definition.setMethod(function toJSON() { let result = { name : this.name, type : this.type, + setting_id : this.setting_id, allowed_values : this.allowed_values, validation_pattern : this.validation_pattern, default_value : this.default_value, show_description : this.show_description, target : this.target, + view_permission : this.view_permission, + edit_persmission : this.edit_persmission, + requires_restart : this.requires_restart, + locked : this.locked, }; return result; @@ -213,6 +271,111 @@ Definition.setMethod(function toEnumEntry() { return result; }); +/** + * Get the configuration for the editor + * (Including the current value) + * + * @author Jelle De Loecker + * @since 1.4.0 + * @version 1.4.0 + * + * @param {Value} root_value + * @param {Conduit} editor_context + * + * @return {Object} + */ +Definition.setMethod(function getEditorConfiguration(root_value, editor_context) { + + let result = this.toEnumEntry(); + + let value = root_value.getPath(this.setting_id); + + if (value) { + result.current_value = value.get(); + result.current_value_is_default = value.has_default_value; + } + + if (result.show_description !== false && this.description) { + result.description = this.description; + } + + if (this.edit_persmission && editor_context && !editor_context.hasPermission(this.edit_persmission)) { + result.locked = true; + } + + return result; +}); + +/** + * Create a configuration object (for storing in the database) + * + * @author Jelle De Loecker + * @since 1.4.0 + * @version 1.4.0 + * + * @param {Mixed} new_value + * + * @return {Object} + */ +Definition.setMethod(function createConfigurationObject(new_value) { + + // @TODO: make sure the value is valid + let result = { + value : new_value, + }; + + return result; +}); + +/** + * Can this setting by edited by the given context? + * + * @author Jelle De Loecker + * @since 1.4.0 + * @version 1.4.0 + * + * @param {Conduit} permission_context + * + * @return {boolean} + */ +Definition.setMethod(function canBeEditedBy(permission_context) { + + // Locked settings can never be edited + if (this.locked) { + return false; + } + + // If no edit permission is required, it can be edited + if (!this.edit_persmission) { + return true; + } + + // If no context is given, it can't be edited + if (!permission_context) { + return false; + } + + return permission_context.hasPermission(this.edit_persmission); +}); + +/** + * Execute the action (if any is linked) + * + * @author Jelle De Loecker + * @since 1.4.0 + * @version 1.4.0 + * + * @param {Alchemy.Setting.Value} value_instance + */ +Definition.setMethod(function executeAction(value_instance) { + + if (!this.action) { + return; + } + + return this.action.call(this, value_instance.get(), value_instance); +}); + /** * The Setting Group class * @@ -221,10 +384,19 @@ Definition.setMethod(function toEnumEntry() { * @version 1.4.0 * * @param {string} name The name of the setting in its (parent) group + * @param {Object} config The settings of this definition * @param {Group} group The parent group */ -const Group = Function.inherits('Alchemy.Setting.Base', function Group(name, group) { - Group.super.call(this, name, group); +const Group = Function.inherits('Alchemy.Setting.Base', function Group(name, config, group) { + + if (!group) { + if (config && config instanceof Group) { + group = config; + config = null; + } + } + + Group.super.call(this, name, config, group); // All the children this.children = new Map(); @@ -253,7 +425,26 @@ Group.setProperty('is_group', true); * @return {Alchemy.Setting.Group|Alchemy.Setting.Definition} */ Group.setMethod(function get(name) { - return this.children.get(name); + + let pieces = name.split('.'), + current = this, + piece; + + if (pieces[0] == current.name) { + pieces.shift(); + } + + while (pieces.length) { + piece = pieces.shift(); + + current = current.children.get(piece); + + if (!current) { + return; + } + } + + return current; }); /** @@ -316,7 +507,7 @@ Group.setMethod(function addSetting(name, config) { */ Group.setMethod(function generateValue() { - const result = new Value(this); + const result = new GroupValue(this); const object = {}; let definition, @@ -363,6 +554,36 @@ Group.setMethod(function setDefaultValue(target, value) { return target; }); +/** + * Set the values silently + * + * @author Jelle De Loecker + * @since 1.4.0 + * @version 1.4.0 + * + * @param {Alchemy.Setting.Value} target + * @param {Mixed} value + */ +Group.setMethod(function setValueSilently(target, value) { + if (!value || typeof value != 'object') { + return; + } + + if (value instanceof Value) { + + if (!value.is_group) { + throw new Error('Cannot set default value of non-group value'); + } + + value = value[VALUE]; + } + + let object = this.assign(target[VALUE], value, false, false); + target[VALUE] = object; + + return target; +}); + /** * Set the value * @@ -388,7 +609,7 @@ Group.setMethod(function setValue(target, value) { value = value[VALUE]; } - let object = this.assign(target[VALUE], value, false); + let object = this.assign(target[VALUE], value, false, true); target[VALUE] = object; return target; @@ -404,8 +625,9 @@ Group.setMethod(function setValue(target, value) { * @param {Object} target * @param {Object} values * @param {boolean} default_only + * @param {boolean} do_actions */ -Group.setMethod(function assign(target, values, default_only) { +Group.setMethod(function assign(target, values, default_only, do_actions = true) { if (!Object.isPlainObject(target)) { if (target.is_group) { @@ -459,7 +681,7 @@ Group.setMethod(function assign(target, values, default_only) { target[key] = group.generateValue(); } - group.assign(target[key], values[key], default_only); + group.assign(target[key], values[key], default_only, do_actions); continue; } @@ -471,8 +693,10 @@ Group.setMethod(function assign(target, values, default_only) { // Set the value by default if (default_only) { current_value.setDefaultValue(values[key]); - } else { + } else if (do_actions) { current_value.setValue(values[key]); + } else { + current_value.setValueSilently(values[key]); } } @@ -546,7 +770,53 @@ Group.setMethod(function createEnumMap() { }); /** - * A value instance of a setting/group + * Get the configuration for the editor + * + * @author Jelle De Loecker + * @since 1.4.0 + * @version 1.4.0 + * + * @param {Value} root_value + * @param {Conduit} editor_context + * + * @return {Object} + */ +Group.setMethod(function getEditorConfiguration(root_value, editor_context) { + + let result = { + is_root : this.group == null, + name : this.name, + group_id : this.setting_id, + settings : [], + children : [], + }; + + let definition, + key; + + for ([key, definition] of this.children) { + + if (definition.view_permission) { + if (editor_context) { + if (!editor_context.hasPermission(definition.view_permission)) { + continue; + } + } + } + + if (definition.is_group) { + result.children.push(definition.getEditorConfiguration(root_value, editor_context)); + continue; + } + + result.settings.push(definition.getEditorConfiguration(root_value, editor_context)); + } + + return result; +}); + +/** + * The base Value class * * @author Jelle De Loecker * @since 1.4.0 @@ -555,17 +825,73 @@ Group.setMethod(function createEnumMap() { * @param {Alchemy.Setting.Base} definition The definition or group */ const Value = Function.inherits('Alchemy.Setting.Base', function Value(definition) { - // The definition of this setting this.definition = definition; +}); - // Is this the default value? - // meaning: this value does not come from any manual override - // (Groups do not have default values) - this.has_default_value = !definition.is_group; +/** + * Mark this as an "abstract" class + * + * @author Jelle De Loecker + * @since 1.4.0 + * @version 1.4.0 + */ +Value.makeAbstractClass(true); - // The actual value - this[VALUE] = definition.is_group ? {} : null; +/** + * Get the setting_id + * + * @author Jelle De Loecker + * @since 1.4.0 + * @version 1.4.0 + * + * @type {string} + */ +Value.setProperty(function setting_id() { + return this.definition.setting_id; +}); + +/** + * Custom Janeway representation (left side) + * + * @author Jelle De Loecker + * @since 1.4.0 + * @version 1.4.0 + * + * @return {string} + */ +Value.setMethod(Symbol.for('janeway_arg_left'), function janewayClassIdentifier() { + return 'A.S.' + this.constructor.name; +}); + +/** + * Custom Janeway representation (right side) + * + * @author Jelle De Loecker + * @since 1.4.0 + * @version 1.4.0 + * + * @return {String} + */ +Value.setMethod(Symbol.for('janeway_arg_right'), function janewayInstanceInfo() { + return this.definition.setting_id; +}); + +/** + * A group value instance + * + * @author Jelle De Loecker + * @since 1.4.0 + * @version 1.4.0 + * + * @param {Alchemy.Setting.Group} group The definition of the group + */ +const GroupValue = Function.inherits('Alchemy.Setting.Value', function GroupValue(group) { + + GroupValue.super.call(this, group); + + this.has_default_value = false; + this[VALUE] = {}; }); /** @@ -577,8 +903,8 @@ const Value = Function.inherits('Alchemy.Setting.Base', function Value(definitio * * @type {boolean} */ -Value.setProperty(function is_group() { - return this.definition.is_group; +GroupValue.setProperty(function is_group() { + return true; }); /** @@ -588,11 +914,7 @@ Value.setProperty(function is_group() { * @since 1.4.0 * @version 1.4.0 */ -Value.setMethod(function toObject() { - - if (!this.is_group) { - return this.get(); - } +GroupValue.setMethod(function toObject() { let result = {}, key; @@ -611,89 +933,116 @@ Value.setMethod(function toObject() { * @since 1.4.0 * @version 1.4.0 */ -Value.setMethod(function toProxyObject() { - - if (!this.is_group) { - throw new Error('Cannot create proxy for non-group value'); - } - +GroupValue.setMethod(function toProxyObject() { return new MagicGroupValue(this); }); /** - * Get the current value + * Set the default value if no value has been set yet * * @author Jelle De Loecker * @since 1.4.0 * @version 1.4.0 */ -Value.setMethod(function get(key) { - let result = this[VALUE]; +GroupValue.setMethod(function setDefaultValue(value) { + this.definition.setDefaultValue(this, value); +}); - if (key != null) { - result = result[key]; - } +/** + * Set the value + * + * @author Jelle De Loecker + * @since 1.4.0 + * @version 1.4.0 + */ +GroupValue.setMethod(function setValue(value) { + this.definition.setValue(this, value); +}); - return result; +/** + * Set the value silently + * + * @author Jelle De Loecker + * @since 1.4.0 + * @version 1.4.0 + */ +GroupValue.setMethod(function setValueSilently(value) { + this.definition.setValueSilently(this, value); }); /** - * Set the default value if no value has been set yet + * Get all the setting values with executable actions in order. * * @author Jelle De Loecker * @since 1.4.0 * @version 1.4.0 + * + * @return {Alchemy.Setting.Value[]} sorted_values */ -Value.setMethod(function setDefaultValue(value) { +GroupValue.setMethod(function getSortedValues() { - if (this.is_group) { - this.definition.setDefaultValue(this, value); - return; - } + let result = this.getFlattenedValues(); - if (!this.has_default_value) { - return; - } + // Try to sort the values by their dependencies, + // aiming to maintain the current order as much as possible. + // This is done by sorting the values by their dependencies, + // and then sorting the dependencies by their dependencies. + // This is done recursively. + result.sortTopological('setting_id', 'all_dependencies'); - if (value != null && typeof value == 'object') { + return result; +}); - if (value instanceof Value) { - value = value[VALUE]; - } +/** + * Add all the values to the given array + * + * @author Jelle De Loecker + * @since 1.4.0 + * @version 1.4.0 + * + * @return {Alchemy.Setting.Value[]} + */ +GroupValue.setMethod(function getFlattenedValues() { - if (value && typeof value == 'object') { - value = JSON.clone(value); + let result = [], + entry, + key; + + for (key in this[VALUE]) { + entry = this[VALUE][key]; + + if (entry.is_group) { + result.push(...entry.getFlattenedValues()); + } else { + result.push(entry); } } - this[VALUE] = value; + return result; }); /** - * Set the value + * Perform all the actions of this group and its children * * @author Jelle De Loecker * @since 1.4.0 * @version 1.4.0 */ -Value.setMethod(function setValue(value) { - - // Even though the default value and this new value might be the same, - // it is no longer considered to be the "default" value! - this.has_default_value = false; +GroupValue.setMethod(async function performAllActions() { - if (this.is_group) { - this.definition.setValue(this, value); - return; - } + let sorted = this.getSortedValues(); - value = this.definition.cast(value); + for (let value of sorted) { + let result = value.executeAction(); - this[VALUE] = value; + if (result) { + await result; + } + } }); /** - * Get via a path + * Get the value at the given path * * @author Jelle De Loecker * @since 1.4.0 @@ -703,16 +1052,24 @@ Value.setMethod(function setValue(value) { * * @return {Value} */ -Value.setMethod(function getPath(path) { +GroupValue.setMethod(function get(path) { if (typeof path == 'string') { path = path.split('.'); } - if (this.is_group && this.definition.group == null && path[0] == this.definition.name) { + if (path && this.definition.group == null && path[0] == this.definition.name) { path.shift(); } + if (!path || path.length == 0) { + return this; + } + + if (path.length == 1) { + return this[VALUE][path[0]]; + } + let current = this, piece; @@ -742,23 +1099,191 @@ Value.setMethod(function getPath(path) { * * @return {Value} */ -Value.setMethod(function setPath(path, raw_value) { +GroupValue.setMethod(function setPath(path, raw_value) { if (typeof path == 'string') { path = path.split('.'); } - if (this.is_group && this.definition.group == null && path[0] == this.definition.name) { + if (this.definition.group == null && path[0] == this.definition.name) { path.shift(); } let object = Object.setPath({}, path, raw_value); - this.setValue(object); + this.setValueSilently(object); return this.getPath(path); }); +/** + * A value instance of a setting + * + * @author Jelle De Loecker + * @since 1.4.0 + * @version 1.4.0 + * + * @param {Alchemy.Setting.Definition} definition The definition of the setting + */ +const SettingValue = Function.inherits('Alchemy.Setting.Value', function SettingValue(definition) { + + SettingValue.super.call(this, definition); + + // Is this the default value? + // meaning: this value does not come from any manual override + // (Groups do not have default values) + this.has_default_value = true; + + // The actual value + this[VALUE] = null; + + // How many times the action has been executed + this.action_execution_count = 0; +}); + +/** + * Get all the dependency ids + * + * @author Jelle De Loecker + * @since 1.4.0 + * @version 1.4.0 + * + * @return {string[]} + */ +SettingValue.setProperty(function all_dependencies() { + return this.definition.all_dependencies; +}); + +/** + * Get this group as a simple object + * + * @author Jelle De Loecker + * @since 1.4.0 + * @version 1.4.0 + */ +SettingValue.setMethod(function toObject() { + return this.get(); +}); + +/** + * Get the current value + * + * @author Jelle De Loecker + * @since 1.4.0 + * @version 1.4.0 + */ +SettingValue.setMethod(function get(key) { + let result = this[VALUE]; + + if (key != null) { + result = result[key]; + } + + return result; +}); + +/** + * Set the default value if no value has been set yet + * + * @author Jelle De Loecker + * @since 1.4.0 + * @version 1.4.0 + */ +SettingValue.setMethod(function setDefaultValue(value) { + + if (!this.has_default_value) { + return; + } + + if (value != null && typeof value == 'object') { + + if (value instanceof Value) { + value = value[VALUE]; + } + + if (value && typeof value == 'object') { + value = JSON.clone(value); + } + } + + this[VALUE] = value; +}); + +/** + * Set the value + * + * @author Jelle De Loecker + * @since 1.4.0 + * @version 1.4.0 + */ +SettingValue.setMethod(function setValueSilently(value) { + + // Even though the default value and this new value might be the same, + // it is no longer considered to be the "default" value! + this.has_default_value = false; + + value = this.definition.cast(value); + + this[VALUE] = value; +}); + +/** + * Test and set the given value + * + * @author Jelle De Loecker + * @since 1.4.0 + * @version 1.4.0 + */ +SettingValue.setMethod(function setValue(value) { + this.setValueSilently(value); + return this.executeAction(); +}); + +/** + * Execute the action (if any is linked) + * + * @author Jelle De Loecker + * @since 1.4.0 + * @version 1.4.0 + */ +SettingValue.setMethod(function executeAction() { + this.action_execution_count++; + return this.definition.executeAction(this); +}); + +/** + * Get via a path + * + * @author Jelle De Loecker + * @since 1.4.0 + * @version 1.4.0 + * + * @param {string|Array} path + * + * @return {Value} + */ +Value.setMethod(function getPath(path) { + + if (typeof path == 'string') { + path = path.split('.'); + } + + let current = this, + piece; + + while (path.length) { + piece = path.shift(); + + current = current.get(piece); + + if (!current) { + return null; + } + } + + return current; +}); + /** * A magic value getter * diff --git a/lib/scripts/create_settings.js b/lib/scripts/create_settings.js index 0096bb0e..d74fd6bb 100644 --- a/lib/scripts/create_settings.js +++ b/lib/scripts/create_settings.js @@ -1,4 +1,5 @@ const SettingNs = Function.getNamespace('Alchemy.Setting'); +const libfs = require('fs'); // Create the system settings group const system = new Classes.Alchemy.Setting.Group('system', null); @@ -9,6 +10,8 @@ system.addSetting('environment', { default : null, values : ['dev', 'live', 'preview'], description : 'The current environment', + locked : true, + requires_restart : true, }); system.addSetting('name', { @@ -26,18 +29,36 @@ network.addSetting('port', { type : 'primitive', default : 3000, description : 'The port or socket to listen to', + locked : true, + requires_restart : true, }); network.addSetting('socket', { type : 'string', default : null, description : 'The optional socket file to listen to (higher priority than port)', + locked : true, + requires_restart : true, }); network.addSetting('socketfile_chmod', { type : 'string', default : null, description : 'The chmod to set on the socket file', + action : function applySocketfileChmod(value, value_instance) { + + if (!value) { + return; + } + + let socketfile = alchemy.getSetting('network.socket'); + + if (!socketfile) { + return; + } + + libfs.chmodSync(socketfile, value); + }, }); network.addSetting('assume_https', { @@ -56,6 +77,7 @@ network.addSetting('use_json_dry_responses', { type : 'boolean', default : true, description : 'Allow use of JSON-dry in non-hawkejs responses', + locked : true, }); network.addSetting('use_websockets', { @@ -101,12 +123,41 @@ performance.addSetting('preload', { type : 'boolean', default : false, description : 'Should the home page & client file be preloaded on boot?', + requires_restart : true, }); performance.addSetting('janeway_lag_menu', { type : 'boolean', default : true, description : 'Show the lag menu entry in Janeway', + requires : 'system.debugging.enable_janeway', + depends_on : [], + action : function applyJanewayLagMenu(value, value_instance) { + + if (!value) { + + // Remove existing entry + if (alchemy.Janeway.lag_menu) { + alchemy.Janeway.lag_menu.remove(); + } + + return; + } + + // Do nothing if it already exists + if (alchemy.Janeway.lag_menu) { + return; + } + + let lag_menu = alchemy.Janeway.addIndicator('0 ms'); + + setInterval(() => { + lag_menu.setIcon(alchemy.lagInMs() + ' ms'); + alchemy.Janeway.redraw(); + }, 900).unref(); + + alchemy.Janeway.lag_menu = lag_menu; + }, }); const debugging = system.createGroup('debugging'); @@ -120,6 +171,8 @@ debugging.addSetting('enable_janeway', { type : 'boolean', default : true, description : 'Enable the Janeway TUI', + locked : true, + requires_restart : true, }); debugging.addSetting('info_page', { @@ -164,6 +217,14 @@ frontend.addSetting('title', { type : 'string', default : null, description : 'The title of the site', + action : function applyTitle(value, value_instance) { + + if (!value || !alchemy.Janeway) { + return; + } + + alchemy.Janeway.setTitle(value); + }, }); frontend.addSetting('title_suffix', { @@ -279,6 +340,36 @@ sessions.addSetting('janeway_menu', { type : 'boolean', default : false, description : 'Show a list of all sessions in the Janeway TUI', + requires : 'system.debugging.enable_janeway', + depends_on : [], + action : function createJanewaySessionMenu(value, value_instance) { + + if (!value) { + + // Remove existing entry + if (alchemy.Janeway.session_menu) { + alchemy.Janeway.session_menu.remove(); + } + + return; + } + + // Do nothing if it already exists + if (alchemy.Janeway.session_menu) { + return; + } + + let session_menu = alchemy.Janeway.addIndicator('⌨ '); + + session_menu.addItem({ + title : 'Current active browser sessions:', + weight : Infinity, + }, () => { + console.log('All sessions:', this.sessions); + }); + + alchemy.Janeway.session_menu = session_menu; + }, }); const data_management = system.createGroup('data_management'); @@ -319,6 +410,13 @@ task.addSetting('janeway_menu', { type : 'boolean', default : true, description : 'Show the task menu in Janeway', + requires : 'system.debugging.enable_janeway', + depends_on : [], + action : function createJanewayTaskMenu(value, value_instance) { + if (value) { + return alchemy.task_service.createJanewayTaskMenu(); + } + }, }); const errors = system.createGroup('errors'); diff --git a/test/00-init.js b/test/00-init.js index 64159546..a8b63f91 100644 --- a/test/00-init.js +++ b/test/00-init.js @@ -324,8 +324,8 @@ global.clickElement = async function clickElement(query) { }; describe('require(\'alchemymvc\')', function() { - it('should create the global alchemy object', function() { - assert.equal('object', typeof alchemy); + it('should create the global STAGES instance', function() { + assert.equal('object', typeof STAGES); }); }); @@ -349,17 +349,20 @@ describe('Alchemy', function() { describe('#start(callback)', function() { it('should start the server', function(done) { - alchemy.settings.port = 3470; - alchemy.settings.postpone_requests_on_overload = false; + alchemy.setSetting('network.port', 3470); + alchemy.setSetting('network.postpone_requests_on_overload', false); - alchemy.start({silent: true}, function started() { + STAGES.getStage('load_core').addPostTask(() => { - setTimeout(function() { - // Also create the mongodb datasource - Datasource.create('mongo', 'default', {uri: mongo_uri}); - }, 50); + alchemy.start({silent: true}, function started() { - done(); + setTimeout(function() { + // Also create the mongodb datasource + Datasource.create('mongo', 'default', {uri: mongo_uri}); + }, 50); + + done(); + }); }); }); });