refacto(backend): regrouped config related handling

This commit is contained in:
Florent Chehab
2020-05-11 14:18:59 +02:00
parent ca47c41c69
commit efaa4b795c
7 changed files with 97 additions and 99 deletions

View File

@@ -0,0 +1,80 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Whiteboard config",
"type": "object",
"properties": {
"backend": {
"type": "object",
"required": ["accessToken", "performance", "webdav"],
"additionalProperties": false,
"properties": {
"accessToken": {
"type": "string"
},
"webdav": {
"type": "boolean"
},
"performance": {
"additionalProperties": false,
"type": "object",
"required": ["whiteboardInfoBroadcastFreq"],
"properties": {
"whiteboardInfoBroadcastFreq": {
"type": "number",
"minimum": 0
}
}
}
}
},
"frontend": {
"type": "object",
"additionalProperties": false,
"required": ["readOnlyOnWhiteboardLoad", "showSmallestScreenIndicator", "performance"],
"properties": {
"readOnlyOnWhiteboardLoad": {
"type": "boolean"
},
"showSmallestScreenIndicator": {
"type": "boolean"
},
"performance": {
"type": "object",
"additionalProperties": false,
"required": ["pointerEventsThrottling", "refreshInfoFreq"],
"properties": {
"pointerEventsThrottling": {
"type": "array",
"minItems": 1,
"items": {
"type": "object",
"additionalProperties": false,
"required": ["fromNbUser", "minDistDelta", "maxFreq"],
"properties": {
"fromNbUser": {
"type": "number",
"minimum": 0
},
"minDistDelta": {
"type": "number",
"minimum": 0
},
"maxFreq": {
"type": "number",
"minimum": 0
}
}
}
},
"refreshInfoFreq": {
"type": "number",
"minimum": 0
}
}
}
}
}
},
"required": ["backend", "frontend"],
"additionalProperties": false
}

80
scripts/config/config.js Normal file
View File

@@ -0,0 +1,80 @@
const util = require("util");
const { getDefaultConfig, getConfig, deepMergeConfigs, isConfigValid } = require("./utils");
const { getArgs } = require("./../utils");
const defaultConfig = getDefaultConfig();
const cliArgs = getArgs();
let userConfig = {};
if (cliArgs["config"]) {
userConfig = getConfig(cliArgs["config"]);
}
const config = deepMergeConfigs(defaultConfig, userConfig);
/**
* Update the config based on the CLI args
* @param {object} startArgs
*/
function updateConfigFromStartArgs(startArgs) {
function deprecateCliArg(key, callback) {
const val = startArgs[key];
if (val) {
console.warn(
"\x1b[33m\x1b[1m",
`Setting config values (${key}) from the CLI is deprecated. ` +
"This ability will be removed in the next major version. " +
"You should use the config file. "
);
callback(val);
}
}
deprecateCliArg("accesstoken", (val) => (config.backend.accessToken = val));
deprecateCliArg(
"disablesmallestscreen",
() => (config.backend.showSmallestScreenIndicator = false)
);
deprecateCliArg("webdav", () => (config.backend.webdav = true));
}
/**
* Update the config based on the env variables
*/
function updateConfigFromEnv() {
function deprecateEnv(key, callback) {
const val = process.env[key];
if (val) {
console.warn(
"\x1b[33m\x1b[1m",
`Setting config values (${key}) from the environment is deprecated. ` +
"This ability will be removed in the next major version. " +
"You should use the config file. "
);
callback(val);
}
}
deprecateEnv("accesstoken", (val) => (config.backend.accessToken = val));
deprecateEnv(
"disablesmallestscreen",
() => (config.backend.showSmallestScreenIndicator = false)
);
deprecateEnv("webdav", () => (config.backend.webdav = true));
}
// compatibility layer
// FIXME: remove this in next major
updateConfigFromEnv();
// FIXME: remove this in next major
updateConfigFromStartArgs(cliArgs);
if (!isConfigValid(config, true)) {
throw new Error("Config is not valid. Check logs for details");
}
console.info(util.inspect(config, { showHidden: false, depth: null, colors: true }));
module.exports = config;

92
scripts/config/utils.js Normal file
View File

@@ -0,0 +1,92 @@
const path = require("path");
const fs = require("fs");
const yaml = require("js-yaml");
const Ajv = require("ajv");
const ajv = new Ajv({ allErrors: true });
const configSchema = require("./config-schema.json");
/**
* Load a yaml config file from a given path.
*
* @param path
* @return {Object}
*/
function getConfig(path) {
return yaml.safeLoad(fs.readFileSync(path, "utf8"));
}
/**
* Check that a config object is valid.
*
* @param {Object} config Config object
* @param {boolean} warn Should we warn in console for errors
* @return {boolean}
*/
function isConfigValid(config, warn = true) {
const validate = ajv.compile(configSchema);
const isValidAgainstSchema = validate(config);
if (!isValidAgainstSchema && warn) console.warn(validate.errors);
let structureIsValid = false;
try {
structureIsValid = config.frontend.performance.pointerEventsThrottling.some(
(item) => item.fromNbUser === 0
);
} catch (e) {
if (!e instanceof TypeError) {
throw e;
}
}
if (!structureIsValid && warn)
console.warn(
"At least one item under frontend.performance.pointerEventsThrottling" +
"must have fromNbUser set to 0"
);
return isValidAgainstSchema && structureIsValid;
}
/**
* Load the default project config
* @return {Object}
*/
function getDefaultConfig() {
const defaultConfigPath = path.join(__dirname, "..", "..", "config.default.yml");
return getConfig(defaultConfigPath);
}
/**
* Deep merge of project config
*
* Objects are merged, not arrays
*
* @param baseConfig
* @param overrideConfig
* @return {Object}
*/
function deepMergeConfigs(baseConfig, overrideConfig) {
const out = {};
Object.entries(baseConfig).forEach(([key, val]) => {
out[key] = val;
if (overrideConfig.hasOwnProperty(key)) {
const overrideVal = overrideConfig[key];
if (typeof val === "object" && !Array.isArray(val) && val !== null) {
out[key] = deepMergeConfigs(val, overrideVal);
} else {
out[key] = overrideVal;
}
}
});
return out;
}
module.exports.getConfig = getConfig;
module.exports.getDefaultConfig = getDefaultConfig;
module.exports.deepMergeConfigs = deepMergeConfigs;
module.exports.isConfigValid = isConfigValid;

View File

@@ -0,0 +1,48 @@
const { getDefaultConfig, deepMergeConfigs, isConfigValid } = require("./utils");
test("Load default config", () => {
const defaultConfig = getDefaultConfig();
expect(typeof defaultConfig).toBe("object");
});
test("Full config override", () => {
const defaultConfig = getDefaultConfig();
expect(deepMergeConfigs(defaultConfig, defaultConfig)).toEqual(defaultConfig);
});
test("Simple partial config override", () => {
expect(deepMergeConfigs({ test: true }, { test: false }).test).toBe(false);
expect(deepMergeConfigs({ test: false }, { test: true }).test).toBe(true);
});
test("Simple deep config override", () => {
expect(deepMergeConfigs({ stage1: { stage2: true } }, { stage1: { stage2: false } })).toEqual({
stage1: { stage2: false },
});
});
test("Complex object config override", () => {
expect(
deepMergeConfigs({ stage1: { stage2: true, stage2b: true } }, { stage1: { stage2: false } })
).toEqual({
stage1: { stage2: false, stage2b: true },
});
});
test("Override default config", () => {
const defaultConfig = getDefaultConfig();
const overrideConfig1 = { frontend: { readOnlyOnWhiteboardLoad: true } };
expect(deepMergeConfigs(defaultConfig, overrideConfig1).frontend.readOnlyOnWhiteboardLoad).toBe(
true
);
});
test("Dumb config is not valid", () => {
expect(isConfigValid({}, false)).toBe(false);
});
test("Default config is valid", () => {
const defaultConfig = getDefaultConfig();
expect(isConfigValid(defaultConfig)).toBe(true);
});