Merge commit '12fce6c' into HEAD

# Conflicts:
#	index.js
#	src/configs/index.js
#	src/configs/machineGroupControl.json
#	src/helper/assetUtils.js
#	src/helper/childRegistrationUtils.js
#	src/helper/configUtils.js
#	src/helper/logger.js
#	src/helper/menuUtils.js
#	src/helper/menuUtils_DEPRECATED.js
#	src/helper/outputUtils.js
#	src/helper/validationUtils.js
#	src/measurements/Measurement.js
#	src/measurements/MeasurementContainer.js
#	src/measurements/examples.js
#	src/outliers/outlierDetection.js
This commit is contained in:
znetsixe
2026-03-31 18:07:57 +02:00
53 changed files with 3545 additions and 1978 deletions

View File

@@ -1,38 +1,52 @@
/**
* @file validation.js
*
* Permission is hereby granted to any person obtaining a copy of this software
* and associated documentation files (the "Software"), to use it for personal
* Permission is hereby granted to any person obtaining a copy of this software
* and associated documentation files (the "Software"), to use it for personal
* or non-commercial purposes, with the following restrictions:
*
* 1. **No Copying or Redistribution**: The Software or any of its parts may not
* be copied, merged, distributed, sublicensed, or sold without explicit
* 1. **No Copying or Redistribution**: The Software or any of its parts may not
* be copied, merged, distributed, sublicensed, or sold without explicit
* prior written permission from the author.
*
* 2. **Commercial Use**: Any use of the Software for commercial purposes requires
*
* 2. **Commercial Use**: Any use of the Software for commercial purposes requires
* a valid license, obtainable only with the explicit consent of the author.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISING FROM,
* OUT OF, OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISING FROM,
* OUT OF, OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
* Ownership of this code remains solely with the original author. Unauthorized
* Ownership of this code remains solely with the original author. Unauthorized
* use of this Software is strictly prohibited.
* @summary Validation utility for validating and constraining configuration values.
* @description Validation utility for validating and constraining configuration values.
* @module ValidationUtils
* @requires Logger
* @exports ValidationUtils
* @version 0.1.0
* @version 0.2.0
* @since 0.1.0
*/
const Logger = require("./logger");
const { validateNumber, validateInteger, validateBoolean, validateString, validateEnum } = require("./validators/typeValidators");
const { validateArray, validateSet, validateObject } = require("./validators/collectionValidators");
const { validateCurve, validateMachineCurve } = require("./validators/curveValidator");
// Strategy registry: maps rules.type to a handler function
const VALIDATORS = {
number: (cv, rules, fs, name, key, logger) => validateNumber(cv, rules, fs, name, key, logger),
integer: (cv, rules, fs, name, key, logger) => validateInteger(cv, rules, fs, name, key, logger),
boolean: (cv, _rules, _fs, name, key, logger) => validateBoolean(cv, name, key, logger),
string: (cv, rules, fs, name, key, logger) => validateString(cv, rules, fs, name, key, logger),
enum: (cv, rules, fs, name, key, logger) => validateEnum(cv, rules, fs, name, key, logger),
array: (cv, rules, fs, name, key, logger) => validateArray(cv, rules, fs, name, key, logger),
set: (cv, rules, fs, name, key, logger) => validateSet(cv, rules, fs, name, key, logger),
};
class ValidationUtils {
constructor(IloggerEnabled, IloggerLevel) {
@@ -77,7 +91,7 @@ class ValidationUtils {
delete config[key];
}
}
// Validate each key in the schema and loop over wildcards if they are not in schema
for ( const key in schema ) {
@@ -87,7 +101,7 @@ class ValidationUtils {
const fieldSchema = schema[key];
const { rules = {} } = fieldSchema;
// Default to the schema's default value if the key is missing
if (config[key] === undefined) {
if (fieldSchema.default === undefined) {
@@ -118,77 +132,58 @@ class ValidationUtils {
configValue = config[key] !== undefined ? config[key] : fieldSchema.default;
}
// Attempt to parse the value to the expected type if possible
switch (rules.type) {
case "number":
configValue = this.validateNumber(configValue, rules, fieldSchema, name, key);
break;
case "boolean":
configValue = this.validateBoolean(configValue, name, key);
break;
case "string":
configValue = this.validateString(configValue,rules,fieldSchema, name, key);
break;
case "array":
configValue = this.validateArray(configValue, rules, fieldSchema, name, key);
break;
case "set":
configValue = this.validateSet(configValue, rules, fieldSchema, name, key);
break;
case "object":
configValue = this.validateObject(configValue, rules, fieldSchema, name, key);
break;
case "enum":
configValue = this.validateEnum(configValue, rules, fieldSchema, name, key);
break;
case "curve":
validatedConfig[key] = this.validateCurve(configValue,fieldSchema.default);
continue;
case "machineCurve":
validatedConfig[key] = this.validateMachineCurve(configValue,fieldSchema.default);
continue;
case "integer":
validatedConfig[key] = this.validateInteger(configValue, rules, fieldSchema, name, key);
continue;
case undefined:
// If we see 'rules.schema' but no 'rules.type', treat it like an object:
if (rules.schema && !rules.type) {
// Log a warning and skip the extra pass for nested schema
this.logger.warn(
`${name}.${key} has a nested schema but no type. ` +
`Treating it as type="object" to skip extra pass.`
);
} else {
// Otherwise, fallback to your existing "validateUndefined" logic
validatedConfig[key] = this.validateUndefined(configValue, fieldSchema, name, key);
}
continue;
default:
this.logger.warn(`${name}.${key} has an unknown validation type: ${rules.type}. Skipping validation.`);
validatedConfig[key] = fieldSchema.default;
continue;
// Handle curve types (they use continue, so handle separately)
if (rules.type === "curve") {
validatedConfig[key] = validateCurve(configValue, fieldSchema.default, this.logger);
continue;
}
if (rules.type === "machineCurve") {
validatedConfig[key] = validateMachineCurve(configValue, fieldSchema.default, this.logger);
continue;
}
// Handle object type (needs recursive validateSchema reference)
if (rules.type === "object") {
validatedConfig[key] = validateObject(
configValue, rules, fieldSchema, name, key,
(c, s, n) => this.validateSchema(c, s, n),
this.logger
);
continue;
}
// Handle undefined type
if (rules.type === undefined) {
if (rules.schema && !rules.type) {
this.logger.warn(
`${name}.${key} has a nested schema but no type. ` +
`Treating it as type="object" to skip extra pass.`
);
} else {
validatedConfig[key] = this.validateUndefined(configValue, fieldSchema, name, key);
}
continue;
}
// Use the strategy registry for all other types
const handler = VALIDATORS[rules.type];
if (handler) {
configValue = handler(configValue, rules, fieldSchema, name, key, this.logger);
} else {
this.logger.warn(`${name}.${key} has an unknown validation type: ${rules.type}. Skipping validation.`);
validatedConfig[key] = fieldSchema.default;
continue;
}
// Assign the validated or converted value
validatedConfig[key] = configValue;
}
// Ignore unknown keys by not processing them at all
this.logger.info(`Validation completed for ${name}.`);
return validatedConfig;
}
}
removeUnwantedKeys(obj) {
@@ -216,358 +211,6 @@ class ValidationUtils {
}
return obj;
}
validateMachineCurve(curve, defaultCurve) {
if (!curve || typeof curve !== "object" || Object.keys(curve).length === 0) {
this.logger.warn("Curve is missing or invalid. Defaulting to basic curve.");
return defaultCurve;
}
// Validate that nq and np exist and are objects
const { nq, np } = curve;
if (!nq || typeof nq !== "object" || !np || typeof np !== "object") {
this.logger.warn("Curve must contain valid 'nq' and 'np' objects. Defaulting to basic curve.");
return defaultCurve;
}
// Validate that each dimension key points to a valid object with x and y arrays
const validatedNq = this.validateDimensionStructure(nq, "nq");
const validatedNp = this.validateDimensionStructure(np, "np");
if (!validatedNq || !validatedNp) {
return defaultCurve;
}
return { nq: validatedNq, np: validatedNp }; // Return the validated curve
}
validateCurve(curve, defaultCurve) {
if (!curve || typeof curve !== "object" || Object.keys(curve).length === 0) {
this.logger.warn("Curve is missing or invalid. Defaulting to basic curve.");
return defaultCurve;
}
// Validate that each dimension key points to a valid object with x and y arrays
const validatedCurve = this.validateDimensionStructure(curve, "curve");
if (!validatedCurve) {
return defaultCurve;
}
return validatedCurve; // Return the validated curve
}
validateDimensionStructure(dimension, name) {
const validatedDimension = {};
for (const [key, value] of Object.entries(dimension)) {
// Validate that each key points to an object with x and y arrays
if (typeof value !== "object") {
this.logger.warn(`Dimension '${name}' key '${key}' is not valid. Returning to default.`);
return false;
}
// Validate that x and y are arrays
else if (!Array.isArray(value.x) || !Array.isArray(value.y)) {
this.logger.warn(`Dimension '${name}' key '${key}' is missing x or y arrays. Converting to arrays.`);
// Try to convert to arrays first
value.x = Object.values(value.x);
value.y = Object.values(value.y);
// If still not arrays return false
if (!Array.isArray(value.x) || !Array.isArray(value.y)) {
this.logger.warn(`Dimension '${name}' key '${key}' is not valid. Returning to default.`);
return false;
}
}
// Validate that x and y arrays are the same length
else if (value.x.length !== value.y.length) {
this.logger.warn(`Dimension '${name}' key '${key}' has mismatched x and y lengths. Ignoring this key.`);
return false;
}
// Validate that x values are in ascending order
else if (!this.isSorted(value.x)) {
this.logger.warn(`Dimension '${name}' key '${key}' has unsorted x values. Sorting...`);
return false;
}
// Validate that x values are unique
else if (!this.isUnique(value.x)) {
this.logger.warn(`Dimension '${name}' key '${key}' has duplicate x values. Removing duplicates...`);
return false;
}
// Validate that y values are numbers
else if (!this.areNumbers(value.y)) {
this.logger.warn(`Dimension '${name}' key '${key}' has non-numeric y values. Ignoring this key.`);
return false;
}
validatedDimension[key] = value;
}
return validatedDimension;
}
isSorted(arr) {
return arr.every((_, i) => i === 0 || arr[i] >= arr[i - 1]);
}
isUnique(arr) {
return new Set(arr).size === arr.length;
}
areNumbers(arr) {
return arr.every((x) => typeof x === "number");
}
validateNumber(configValue, rules, fieldSchema, name, key) {
if (typeof configValue !== "number") {
const parsedValue = parseFloat(configValue);
if (!isNaN(parsedValue)) {
this.logger.warn(`${name}.${key} was parsed to a number: ${configValue} -> ${parsedValue}`);
configValue = parsedValue;
}
}
if (rules.min !== undefined && configValue < rules.min) {
this.logger.warn(
`${name}.${key} is below the minimum (${rules.min}). Using default value.`
);
return fieldSchema.default;
}
if (rules.max !== undefined && configValue > rules.max) {
this.logger.warn(
`${name}.${key} exceeds the maximum (${rules.max}). Using default value.`
);
return fieldSchema.default;
}
this.logger.debug(`${name}.${key} is a valid number: ${configValue}`);
return configValue;
}
validateInteger(configValue, rules, fieldSchema, name, key) {
if (typeof configValue !== "number" || !Number.isInteger(configValue)) {
const parsedValue = parseInt(configValue, 10);
if (!isNaN(parsedValue) && Number.isInteger(parsedValue)) {
this.logger.warn(`${name}.${key} was parsed to an integer: ${configValue} -> ${parsedValue}`);
configValue = parsedValue;
} else {
this.logger.warn(`${name}.${key} is not a valid integer. Using default value.`);
return fieldSchema.default;
}
}
if (rules.min !== undefined && configValue < rules.min) {
this.logger.warn(`${name}.${key} is below the minimum integer value (${rules.min}). Using default value.`);
return fieldSchema.default;
}
if (rules.max !== undefined && configValue > rules.max) {
this.logger.warn(`${name}.${key} exceeds the maximum integer value (${rules.max}). Using default value.`);
return fieldSchema.default;
}
this.logger.debug(`${name}.${key} is a valid integer: ${configValue}`);
return configValue;
}
validateBoolean(configValue, name, key) {
if (typeof configValue !== "boolean") {
if (configValue === "true" || configValue === "false") {
const parsedValue = configValue === "true";
this.logger.debug(`${name}.${key} was parsed to a boolean: ${configValue} -> ${parsedValue}`);
configValue = parsedValue;
}
}
return configValue;
}
validateString(configValue, rules, fieldSchema, name, key) {
let newConfigValue = configValue;
if (typeof configValue !== "string") {
//check if the value is nullable
if(rules.nullable){
if(configValue === null){
return null;
}
}
this.logger.warn(`${name}.${key} is not a string. Trying to convert to string.`);
newConfigValue = String(configValue); // Coerce to string if not already
}
//check if the string is a valid string after conversion
if (typeof newConfigValue !== "string") {
this.logger.warn(`${name}.${key} is not a valid string. Using default value.`);
return fieldSchema.default;
}
const keyString = `${name}.${key}`;
const normalizeMode = rules.normalize || this._resolveStringNormalizeMode(keyString);
const preserveCase = normalizeMode !== "lowercase";
// Check for uppercase characters and convert to lowercase if present
if (!preserveCase && newConfigValue !== newConfigValue.toLowerCase()) {
this._logOnce(
"info",
`normalize-lowercase:${keyString}`,
`${name}.${key} normalized to lowercase: ${newConfigValue} -> ${newConfigValue.toLowerCase()}`
);
newConfigValue = newConfigValue.toLowerCase();
}
return newConfigValue;
}
_isUnitLikeField(path) {
const normalized = String(path || "").toLowerCase();
if (!normalized) return false;
return /(^|\.)([a-z0-9]*unit|units)(\.|$)/.test(normalized)
|| normalized.includes(".curveunits.");
}
_resolveStringNormalizeMode(path) {
const normalized = String(path || "").toLowerCase();
if (!normalized) return "none";
if (this._isUnitLikeField(normalized)) return "none";
if (normalized.endsWith(".name")) return "none";
if (normalized.endsWith(".model")) return "none";
if (normalized.endsWith(".supplier")) return "none";
if (normalized.endsWith(".role")) return "none";
if (normalized.endsWith(".description")) return "none";
if (normalized.endsWith(".softwaretype")) return "lowercase";
if (normalized.endsWith(".type")) return "lowercase";
if (normalized.endsWith(".category")) return "lowercase";
return "lowercase";
}
validateSet(configValue, rules, fieldSchema, name, key) {
// 1. Ensure we have a Set. If not, use default.
if (!(configValue instanceof Set)) {
this.logger.debug(`${name}.${key} is not a Set. Converting to one using default value.`);
return new Set(fieldSchema.default);
}
// 2. Convert the Set to an array for easier filtering.
const validatedArray = [...configValue]
.filter((item) => {
// 3. Filter based on `rules.itemType`.
switch (rules.itemType) {
case "number":
return typeof item === "number";
case "string":
return typeof item === "string";
case "null":
// "null" might mean no type restriction (your usage may vary).
return true;
default:
// Fallback if itemType is something else
return typeof item === rules.itemType;
}
})
.slice(0, rules.maxLength || Infinity);
// 4. Check if the filtered array meets the minimum length.
const minLength = Number.isInteger(rules.minLength) ? rules.minLength : 0;
if (validatedArray.length < minLength) {
this.logger.warn(
`${name}.${key} contains fewer items than allowed (${minLength}). Using default value.`
);
return new Set(fieldSchema.default);
}
// 5. Return a new Set containing only the valid items.
return new Set(validatedArray);
}
validateArray(configValue, rules, fieldSchema, name, key) {
if (!Array.isArray(configValue)) {
this.logger.debug(`${name}.${key} is not an array. Using default value.`);
return fieldSchema.default;
}
// Validate individual items in the array
const validatedArray = configValue
.filter((item) => {
switch (rules.itemType) {
case "number":
return typeof item === "number";
case "string":
return typeof item === "string";
case "null":
// anything goes
return true;
default:
return typeof item === rules.itemType;
}
})
.slice(0, rules.maxLength || Infinity);
const minLength = Number.isInteger(rules.minLength) ? rules.minLength : 0;
if (validatedArray.length < minLength) {
this.logger.warn(
`${name}.${key} contains fewer items than allowed (${minLength}). Using default value.`
);
return fieldSchema.default;
}
return validatedArray;
}
validateObject(configValue, rules, fieldSchema, name, key) {
if (typeof configValue !== "object" || Array.isArray(configValue)) {
this.logger.warn(`${name}.${key} is not a valid object. Using default value.`);
return fieldSchema.default;
}
if (rules.schema) {
// Recursively validate nested objects if a schema is defined
return this.validateSchema(configValue || {}, rules.schema, `${name}.${key}`);
} else {
// If no schema is defined, log a warning and use the default
this.logger.warn(`${name}.${key} is an object with no schema. Using default value.`);
return fieldSchema.default;
}
}
validateEnum(configValue, rules, fieldSchema, name, key) {
if (Array.isArray(rules.values)) {
//if value is null take default
if(configValue === null){
this.logger.warn(`${name}.${key} is null. Using default value.`);
return fieldSchema.default;
}
if (typeof configValue !== "string") {
this.logger.warn(`${name}.${key} is not a valid enum string. Using default value.`);
return fieldSchema.default;
}
const validValues = rules.values.map(e => e.value.toLowerCase());
//remove caps
configValue = configValue.toLowerCase();
if (!validValues.includes(configValue)) {
this.logger.warn(
`${name}.${key} has an invalid value : ${configValue}. Allowed values: [${validValues.join(", ")}]. Using default value.`
);
return fieldSchema.default;
}
} else {
this.logger.warn(
`${name}.${key} is an enum with no 'values' array. Using default value.`
);
return fieldSchema.default;
}
return configValue;
}
validateUndefined(configValue, fieldSchema, name, key) {
if (typeof configValue === "object" && !Array.isArray(configValue)) {
@@ -576,7 +219,7 @@ class ValidationUtils {
// Recursively validate the nested object
return this.validateSchema( configValue || {}, fieldSchema, `${name}.${key}`);
}
}
else {
this.logger.warn(`${name}.${key} has no defined rules. Using default value.`);
return fieldSchema.default;