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

@@ -0,0 +1,66 @@
/**
* Standalone collection validation functions extracted from validationUtils.js.
*/
function validateArray(configValue, rules, fieldSchema, name, key, logger) {
if (!Array.isArray(configValue)) {
logger.info(`${name}.${key} is not an array. Using default value.`);
return fieldSchema.default;
}
const validatedArray = configValue
.filter((item) => {
switch (rules.itemType) {
case "number": return typeof item === "number";
case "string": return typeof item === "string";
case "null": return true;
default: return typeof item === rules.itemType;
}
})
.slice(0, rules.maxLength || Infinity);
if (validatedArray.length < (rules.minLength || 1)) {
logger.warn(
`${name}.${key} contains fewer items than allowed (${rules.minLength}). Using default value.`
);
return fieldSchema.default;
}
return validatedArray;
}
function validateSet(configValue, rules, fieldSchema, name, key, logger) {
if (!(configValue instanceof Set)) {
logger.info(`${name}.${key} is not a Set. Converting to one using default value.`);
return new Set(fieldSchema.default);
}
const validatedArray = [...configValue]
.filter((item) => {
switch (rules.itemType) {
case "number": return typeof item === "number";
case "string": return typeof item === "string";
case "null": return true;
default: return typeof item === rules.itemType;
}
})
.slice(0, rules.maxLength || Infinity);
if (validatedArray.length < (rules.minLength || 1)) {
logger.warn(
`${name}.${key} contains fewer items than allowed (${rules.minLength}). Using default value.`
);
return new Set(fieldSchema.default);
}
return new Set(validatedArray);
}
function validateObject(configValue, rules, fieldSchema, name, key, validateSchemaFn, logger) {
if (typeof configValue !== "object" || Array.isArray(configValue)) {
logger.warn(`${name}.${key} is not a valid object. Using default value.`);
return fieldSchema.default;
}
if (rules.schema) {
return validateSchemaFn(configValue || {}, rules.schema, `${name}.${key}`);
} else {
logger.warn(`${name}.${key} is an object with no schema. Using default value.`);
return fieldSchema.default;
}
}
module.exports = { validateArray, validateSet, validateObject };

View File

@@ -0,0 +1,108 @@
/**
* Curve validation strategies for machine curves and generic curves.
* Extracted from validationUtils.js for modularity.
*/
function isSorted(arr) {
return arr.every((_, i) => i === 0 || arr[i] >= arr[i - 1]);
}
function isUnique(arr) {
return new Set(arr).size === arr.length;
}
function areNumbers(arr) {
return arr.every((x) => typeof x === "number");
}
function validateDimensionStructure(dimension, name, logger) {
const validatedDimension = {};
for (const [key, value] of Object.entries(dimension)) {
if (typeof value !== "object") {
logger.warn(`Dimension '${name}' key '${key}' is not valid. Returning to default.`);
return false;
}
else if (!Array.isArray(value.x) || !Array.isArray(value.y)) {
logger.warn(`Dimension '${name}' key '${key}' is missing x or y arrays. Converting to arrays.`);
value.x = Object.values(value.x);
value.y = Object.values(value.y);
if (!Array.isArray(value.x) || !Array.isArray(value.y)) {
logger.warn(`Dimension '${name}' key '${key}' is not valid. Returning to default.`);
return false;
}
}
else if (value.x.length !== value.y.length) {
logger.warn(`Dimension '${name}' key '${key}' has mismatched x and y lengths. Ignoring this key.`);
return false;
}
else if (!isSorted(value.x)) {
logger.warn(`Dimension '${name}' key '${key}' has unsorted x values. Sorting...`);
const indices = value.x.map((_v, i) => i);
indices.sort((a, b) => value.x[a] - value.x[b]);
value.x = indices.map(i => value.x[i]);
value.y = indices.map(i => value.y[i]);
}
if (!isUnique(value.x)) {
logger.warn(`Dimension '${name}' key '${key}' has duplicate x values. Removing duplicates...`);
const seen = new Set();
const uniqueX = [];
const uniqueY = [];
for (let i = 0; i < value.x.length; i++) {
if (!seen.has(value.x[i])) {
seen.add(value.x[i]);
uniqueX.push(value.x[i]);
uniqueY.push(value.y[i]);
}
}
value.x = uniqueX;
value.y = uniqueY;
}
if (!areNumbers(value.y)) {
logger.warn(`Dimension '${name}' key '${key}' has non-numeric y values. Ignoring this key.`);
return false;
}
validatedDimension[key] = value;
}
return validatedDimension;
}
function validateCurve(configValue, defaultCurve, logger) {
if (!configValue || typeof configValue !== "object" || Object.keys(configValue).length === 0) {
logger.warn("Curve is missing or invalid. Defaulting to basic curve.");
return defaultCurve;
}
const validatedCurve = validateDimensionStructure(configValue, "curve", logger);
if (!validatedCurve) {
return defaultCurve;
}
return validatedCurve;
}
function validateMachineCurve(configValue, defaultCurve, logger) {
if (!configValue || typeof configValue !== "object" || Object.keys(configValue).length === 0) {
logger.warn("Curve is missing or invalid. Defaulting to basic curve.");
return defaultCurve;
}
const { nq, np } = configValue;
if (!nq || typeof nq !== "object" || !np || typeof np !== "object") {
logger.warn("Curve must contain valid 'nq' and 'np' objects. Defaulting to basic curve.");
return defaultCurve;
}
const validatedNq = validateDimensionStructure(nq, "nq", logger);
const validatedNp = validateDimensionStructure(np, "np", logger);
if (!validatedNq || !validatedNp) {
return defaultCurve;
}
return { nq: validatedNq, np: validatedNp };
}
module.exports = {
validateCurve,
validateMachineCurve,
validateDimensionStructure,
isSorted,
isUnique,
areNumbers
};

View File

@@ -0,0 +1,158 @@
/**
* Standalone type validation functions extracted from validationUtils.js.
*/
function validateNumber(configValue, rules, fieldSchema, name, key, logger) {
if (typeof configValue !== "number") {
const parsedValue = parseFloat(configValue);
if (!isNaN(parsedValue)) {
logger.warn(`${name}.${key} was parsed to a number: ${configValue} -> ${parsedValue}`);
configValue = parsedValue;
}
}
if (rules.min !== undefined && configValue < rules.min) {
logger.warn(`${name}.${key} is below the minimum (${rules.min}). Using default value.`);
return fieldSchema.default;
}
if (rules.max !== undefined && configValue > rules.max) {
logger.warn(`${name}.${key} exceeds the maximum (${rules.max}). Using default value.`);
return fieldSchema.default;
}
logger.debug(`${name}.${key} is a valid number: ${configValue}`);
return configValue;
}
function validateInteger(configValue, rules, fieldSchema, name, key, logger) {
if (typeof configValue !== "number" || !Number.isInteger(configValue)) {
const parsedValue = parseInt(configValue, 10);
if (!isNaN(parsedValue) && Number.isInteger(parsedValue)) {
logger.warn(`${name}.${key} was parsed to an integer: ${configValue} -> ${parsedValue}`);
configValue = parsedValue;
} else {
logger.warn(`${name}.${key} is not a valid integer. Using default value.`);
return fieldSchema.default;
}
}
if (rules.min !== undefined && configValue < rules.min) {
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) {
logger.warn(`${name}.${key} exceeds the maximum integer value (${rules.max}). Using default value.`);
return fieldSchema.default;
}
logger.debug(`${name}.${key} is a valid integer: ${configValue}`);
return configValue;
}
function validateBoolean(configValue, name, key, logger) {
if (typeof configValue !== "boolean") {
if (configValue === "true" || configValue === "false") {
const parsedValue = configValue === "true";
logger.debug(`${name}.${key} was parsed to a boolean: ${configValue} -> ${parsedValue}`);
configValue = parsedValue;
}
}
return configValue;
}
function _isUnitLikeField(path) {
const normalized = String(path || "").toLowerCase();
if (!normalized) return false;
return /(^|\.)([a-z0-9]*unit|units)(\.|$)/.test(normalized)
|| normalized.includes(".curveunits.");
}
function _resolveStringNormalizeMode(path) {
const normalized = String(path || "").toLowerCase();
if (!normalized) return "none";
if (_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";
}
function validateString(configValue, rules, fieldSchema, name, key, logger) {
let newConfigValue = configValue;
if (typeof configValue !== "string") {
//check if the value is nullable
if(rules.nullable){
if(configValue === null){
return null;
}
}
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") {
logger.warn(`${name}.${key} is not a valid string. Using default value.`);
return fieldSchema.default;
}
const keyString = `${name}.${key}`;
const normalizeMode = rules.normalize || _resolveStringNormalizeMode(keyString);
const preserveCase = normalizeMode !== "lowercase";
// Check for uppercase characters and convert to lowercase if present
if (!preserveCase && newConfigValue !== newConfigValue.toLowerCase()) {
logger.info(
`${name}.${key} normalized to lowercase: ${newConfigValue} -> ${newConfigValue.toLowerCase()}`
);
newConfigValue = newConfigValue.toLowerCase();
}
return newConfigValue;
}
function validateEnum(configValue, rules, fieldSchema, name, key, logger) {
if (Array.isArray(rules.values)) {
//if value is null take default
if(configValue === null){
logger.warn(`${name}.${key} is null. Using default value.`);
return fieldSchema.default;
}
if (typeof configValue !== "string") {
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)) {
logger.warn(
`${name}.${key} has an invalid value : ${configValue}. Allowed values: [${validValues.join(", ")}]. Using default value.`
);
return fieldSchema.default;
}
} else {
logger.warn(
`${name}.${key} is an enum with no 'values' array. Using default value.`
);
return fieldSchema.default;
}
return configValue;
}
module.exports = {
validateNumber,
validateInteger,
validateBoolean,
validateString,
validateEnum,
};