feat(backend): new config handling based on file
* Config should be provided as Yaml file now * Other way to provide configuration are now deprectaed * The config format is checked agaist a schema with the json-schema standard * Tests are are added to the project (in the backend for config parsing)
This commit is contained in:
parent
4476ce3284
commit
0b1d1943ec
17
config.default.yml
Normal file
17
config.default.yml
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
backend:
|
||||||
|
# TODO
|
||||||
|
accessToken: ""
|
||||||
|
# TODO
|
||||||
|
webdav: false
|
||||||
|
performance:
|
||||||
|
# Whiteboard information broadcasting frequency (in /s)
|
||||||
|
# => diminishing this will result in more latency
|
||||||
|
whiteboardInfoBroadcastFreq: 1
|
||||||
|
|
||||||
|
frontend:
|
||||||
|
# When an editable whiteboard is loading in a client,
|
||||||
|
# should it be started in read-only mode.
|
||||||
|
setReadOnlyOnWhiteboardLoad: false
|
||||||
|
# Show smallest screen indicator
|
||||||
|
showSmallestScreenIndicator: true
|
||||||
|
# performance:
|
4033
package-lock.json
generated
4033
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -8,7 +8,7 @@
|
|||||||
"build": "webpack --config config/webpack.build.js",
|
"build": "webpack --config config/webpack.build.js",
|
||||||
"start:dev": "node scripts/server.js --mode=development",
|
"start:dev": "node scripts/server.js --mode=development",
|
||||||
"start:prod": "npm run build && node scripts/server.js --mode=production",
|
"start:prod": "npm run build && node scripts/server.js --mode=production",
|
||||||
"test": "echo \"No tests needed!\" && exit 1",
|
"test": "jest",
|
||||||
"pretty-quick": "pretty-quick",
|
"pretty-quick": "pretty-quick",
|
||||||
"format": "prettier --write .",
|
"format": "prettier --write .",
|
||||||
"style": "prettier --check ."
|
"style": "prettier --check ."
|
||||||
@ -28,10 +28,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"ajv": "6.12.2",
|
||||||
"dompurify": "^2.0.7",
|
"dompurify": "^2.0.7",
|
||||||
"express": "4.*",
|
"express": "4.*",
|
||||||
"formidable": "1.*",
|
"formidable": "1.*",
|
||||||
"fs-extra": "7.*",
|
"fs-extra": "7.*",
|
||||||
|
"js-yaml": "3.13.1",
|
||||||
"jsdom": "^14.0.0",
|
"jsdom": "^14.0.0",
|
||||||
"pdfjs-dist": "^2.3.200",
|
"pdfjs-dist": "^2.3.200",
|
||||||
"socket.io": "2.*",
|
"socket.io": "2.*",
|
||||||
@ -54,6 +56,7 @@
|
|||||||
"css-loader": "^3.5.2",
|
"css-loader": "^3.5.2",
|
||||||
"html-webpack-plugin": "^4.2.0",
|
"html-webpack-plugin": "^4.2.0",
|
||||||
"husky": "^4.2.5",
|
"husky": "^4.2.5",
|
||||||
|
"jest": "26.0.1",
|
||||||
"jquery": "^3.2.1",
|
"jquery": "^3.2.1",
|
||||||
"jquery-ui": "^1.12.1",
|
"jquery-ui": "^1.12.1",
|
||||||
"keymage": "^1.1.3",
|
"keymage": "^1.1.3",
|
||||||
|
@ -85,7 +85,7 @@ class WhiteboardServerSideInfo {
|
|||||||
nbConnectedUsers: this._nbConnectedUsers,
|
nbConnectedUsers: this._nbConnectedUsers,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!config.disableSmallestScreen) {
|
if (config.frontend.showSmallestScreenIndicator) {
|
||||||
out.smallestScreenResolution = this.getSmallestScreenResolution();
|
out.smallestScreenResolution = this.getSmallestScreenResolution();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
46
scripts/config-schema.json
Normal file
46
scripts/config-schema.json
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
{
|
||||||
|
"$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": ["setReadOnlyOnWhiteboardLoad", "showSmallestScreenIndicator"],
|
||||||
|
"properties": {
|
||||||
|
"setReadOnlyOnWhiteboardLoad": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"showSmallestScreenIndicator": {
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["backend", "frontend"],
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
@ -1,45 +1,84 @@
|
|||||||
const { getArgs } = require("./utils");
|
const util = require("util");
|
||||||
|
|
||||||
const config = {
|
const {
|
||||||
accessToken: "",
|
getArgs,
|
||||||
disableSmallestScreen: false,
|
getDefaultConfig,
|
||||||
webdav: false,
|
getConfig,
|
||||||
|
deepMergeConfigs,
|
||||||
|
isConfigValid,
|
||||||
|
} = require("./utils");
|
||||||
|
|
||||||
whiteboardInfoBroadcastFreq: 1, // once per second
|
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
|
* Update the config based on the CLI args
|
||||||
* @param {object} startArgs
|
* @param {object} startArgs
|
||||||
*/
|
*/
|
||||||
function updateConfigFromStartArgs(startArgs) {
|
function updateConfigFromStartArgs(startArgs) {
|
||||||
if (startArgs["accesstoken"]) {
|
function deprecateCliArg(key, callback) {
|
||||||
config.accessToken = startArgs["accesstoken"];
|
const val = startArgs[key];
|
||||||
}
|
if (val) {
|
||||||
if (startArgs["disablesmallestscreen"]) {
|
console.warn(
|
||||||
config.disableSmallestScreen = true;
|
"\x1b[33m\x1b[1m",
|
||||||
}
|
`Setting config values (${key}) from the CLI is deprecated. ` +
|
||||||
if (startArgs["webdav"]) {
|
"This ability will be removed in the next major version. " +
|
||||||
config.webdav = true;
|
"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
|
* Update the config based on the env variables
|
||||||
*/
|
*/
|
||||||
function updateConfigFromEnv() {
|
function updateConfigFromEnv() {
|
||||||
if (process.env.accesstoken) {
|
function deprecateEnv(key, callback) {
|
||||||
config.accessToken = process.env.accesstoken;
|
const val = process.env[key];
|
||||||
}
|
if (val) {
|
||||||
if (process.env.disablesmallestscreen) {
|
console.warn(
|
||||||
config.disablesmallestscreen = true;
|
"\x1b[33m\x1b[1m",
|
||||||
}
|
`Setting config values (${key}) from the environment is deprecated. ` +
|
||||||
if (process.env.webdav) {
|
"This ability will be removed in the next major version. " +
|
||||||
config.webdav = true;
|
"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();
|
updateConfigFromEnv();
|
||||||
updateConfigFromStartArgs(getArgs());
|
// 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;
|
module.exports = config;
|
||||||
|
@ -4,8 +4,6 @@ const config = require("./config");
|
|||||||
const WhiteboardServerSideInfo = require("./WhiteboardServerSideInfo");
|
const WhiteboardServerSideInfo = require("./WhiteboardServerSideInfo");
|
||||||
|
|
||||||
function startBackendServer(port) {
|
function startBackendServer(port) {
|
||||||
console.info("Starting backend server with config", config);
|
|
||||||
|
|
||||||
var fs = require("fs-extra");
|
var fs = require("fs-extra");
|
||||||
var express = require("express");
|
var express = require("express");
|
||||||
var formidable = require("formidable"); //form upload processing
|
var formidable = require("formidable"); //form upload processing
|
||||||
@ -27,7 +25,7 @@ function startBackendServer(port) {
|
|||||||
var io = require("socket.io")(server, { path: "/ws-api" });
|
var io = require("socket.io")(server, { path: "/ws-api" });
|
||||||
console.log("Webserver & socketserver running on port:" + port);
|
console.log("Webserver & socketserver running on port:" + port);
|
||||||
|
|
||||||
const { accessToken, webdav } = config;
|
const { accessToken, webdav } = config.backend;
|
||||||
|
|
||||||
app.get("/api/loadwhiteboard", function (req, res) {
|
app.get("/api/loadwhiteboard", function (req, res) {
|
||||||
var wid = req["query"]["wid"];
|
var wid = req["query"]["wid"];
|
||||||
@ -191,7 +189,7 @@ function startBackendServer(port) {
|
|||||||
info.infoWasSent();
|
info.infoWasSent();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}, (1 / config.whiteboardInfoBroadcastFreq) * 1000);
|
}, (1 / config.backend.performance.whiteboardInfoBroadcastFreq) * 1000);
|
||||||
|
|
||||||
io.on("connection", function (socket) {
|
io.on("connection", function (socket) {
|
||||||
var whiteboardId = null;
|
var whiteboardId = null;
|
||||||
|
@ -1,3 +1,12 @@
|
|||||||
|
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");
|
||||||
|
|
||||||
function getArgs() {
|
function getArgs() {
|
||||||
const args = {};
|
const args = {};
|
||||||
process.argv.slice(2, process.argv.length).forEach((arg) => {
|
process.argv.slice(2, process.argv.length).forEach((arg) => {
|
||||||
@ -17,4 +26,68 @@ function getArgs() {
|
|||||||
return args;
|
return args;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TODO
|
||||||
|
*
|
||||||
|
* @param path
|
||||||
|
* @return {any}
|
||||||
|
*/
|
||||||
|
function getConfig(path) {
|
||||||
|
return yaml.safeLoad(fs.readFileSync(path, "utf8"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TODO
|
||||||
|
* @param config
|
||||||
|
* @param warn
|
||||||
|
* @return {*}
|
||||||
|
*/
|
||||||
|
function isConfigValid(config, warn = true) {
|
||||||
|
const validate = ajv.compile(configSchema);
|
||||||
|
const isValid = validate(config);
|
||||||
|
|
||||||
|
if (!isValid && warn) console.warn(validate.errors);
|
||||||
|
|
||||||
|
return isValid;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TODO
|
||||||
|
* @return {*}
|
||||||
|
*/
|
||||||
|
function getDefaultConfig() {
|
||||||
|
const defaultConfigPath = path.join(__dirname, "..", "config.default.yml");
|
||||||
|
return getConfig(defaultConfigPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TODO
|
||||||
|
* Deep merges objects, not arrays.
|
||||||
|
*
|
||||||
|
* @param baseConfig
|
||||||
|
* @param overrideConfig
|
||||||
|
* @return {{}}
|
||||||
|
*/
|
||||||
|
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.getArgs = getArgs;
|
module.exports.getArgs = getArgs;
|
||||||
|
module.exports.getConfig = getConfig;
|
||||||
|
module.exports.getDefaultConfig = getDefaultConfig;
|
||||||
|
module.exports.deepMergeConfigs = deepMergeConfigs;
|
||||||
|
module.exports.isConfigValid = isConfigValid;
|
||||||
|
48
scripts/utils.test.js
Normal file
48
scripts/utils.test.js
Normal 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: { setReadOnlyOnWhiteboardLoad: true } };
|
||||||
|
|
||||||
|
expect(
|
||||||
|
deepMergeConfigs(defaultConfig, overrideConfig1).frontend.setReadOnlyOnWhiteboardLoad
|
||||||
|
).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);
|
||||||
|
});
|
Loading…
Reference in New Issue
Block a user