changed the folder and added index.js
This commit is contained in:
3
src/helper/assetUtils.js
Normal file
3
src/helper/assetUtils.js
Normal file
@@ -0,0 +1,3 @@
|
||||
export function getAssetVariables() {
|
||||
|
||||
}
|
||||
243
src/helper/childRegistrationUtils.js
Normal file
243
src/helper/childRegistrationUtils.js
Normal file
@@ -0,0 +1,243 @@
|
||||
// ChildRegistrationUtils.js
|
||||
class ChildRegistrationUtils {
|
||||
constructor(mainClass) {
|
||||
this.mainClass = mainClass; // Reference to the main class
|
||||
this.logger = mainClass.logger;
|
||||
}
|
||||
|
||||
async registerChild(child, positionVsParent) {
|
||||
const { softwareType } = child.config.functionality;
|
||||
const { name, id, unit } = child.config.general;
|
||||
const { type = "", subType = "" } = child.config.asset || {};
|
||||
const emitter = child.emitter;
|
||||
|
||||
//define position vs parent in child
|
||||
child.positionVsParent = positionVsParent;
|
||||
child.parent = this.mainClass;
|
||||
|
||||
if (!this.mainClass.child) this.mainClass.child = {};
|
||||
if (!this.mainClass.child[softwareType])
|
||||
this.mainClass.child[softwareType] = {};
|
||||
if (!this.mainClass.child[softwareType][type])
|
||||
this.mainClass.child[softwareType][type] = {};
|
||||
if (!this.mainClass.child[softwareType][type][subType])
|
||||
this.mainClass.child[softwareType][type][subType] = {};
|
||||
|
||||
// Use an array to handle multiple subtypes
|
||||
if (!Array.isArray(this.mainClass.child[softwareType][type][subType])) {
|
||||
this.mainClass.child[softwareType][type][subType] = [];
|
||||
}
|
||||
|
||||
// Update the child in the cloud when available and supply the new child on base of tagcode OLIFANT WE NEED TO FIX THIS SO ITS DYNAMIC!
|
||||
/*
|
||||
try{
|
||||
const url = "https://pimmoerman.nl/rdlab/tagcode.app/v2.1/api/asset/create_asset.php?";
|
||||
const TagCode = child.config.asset.tagCode;
|
||||
//console.log(`Register child => ${TagCode}`);
|
||||
const completeURL = url + `asset_product_model_id=1&asset_product_model_uuid=123456789&asset_name=AssetNaam&asset_description=Beschrijving&asset_status=actief&asset_profile_id=1&asset_location_id=1&asset_process_id=11&asset_tag_number=${TagCode}&child_assets=[L6616]`;
|
||||
|
||||
await fetch(completeURL, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
}catch(e){
|
||||
console.log("Error saving assetID and tagnumber", e);
|
||||
}*/
|
||||
|
||||
// Push the new child to the array of the mainclass so we can track the childs
|
||||
this.mainClass.child[softwareType][type][subType].push({
|
||||
name,
|
||||
id,
|
||||
unit,
|
||||
emitter,
|
||||
});
|
||||
|
||||
//then connect the child depending on the type subtype etc..
|
||||
this.connectChild(
|
||||
id,
|
||||
softwareType,
|
||||
emitter,
|
||||
type,
|
||||
child,
|
||||
subType,
|
||||
positionVsParent
|
||||
);
|
||||
}
|
||||
|
||||
connectChild(
|
||||
id,
|
||||
softwareType,
|
||||
emitter,
|
||||
type,
|
||||
child,
|
||||
subType,
|
||||
positionVsParent
|
||||
) {
|
||||
this.logger.debug(
|
||||
`Connecting child id=${id}: desc=${softwareType}, type=${type},subType=${subType}, position=${positionVsParent}`
|
||||
);
|
||||
|
||||
switch (softwareType) {
|
||||
case "measurement":
|
||||
this.logger.debug(
|
||||
`Registering measurement child: ${id} with type=${type}`
|
||||
);
|
||||
this.connectMeasurement(child, subType, positionVsParent);
|
||||
break;
|
||||
|
||||
case "machine":
|
||||
this.logger.debug(`Registering complete machine child: ${id}`);
|
||||
this.connectMachine(child);
|
||||
break;
|
||||
|
||||
case "valve":
|
||||
this.logger.debug(`Registering complete valve child: ${id}`);
|
||||
this.connectValve(child);
|
||||
break;
|
||||
|
||||
case "actuator":
|
||||
this.logger.debug(`Registering linear actuator child: ${id}`);
|
||||
this.connectActuator(child,positionVsParent);
|
||||
break;
|
||||
|
||||
default:
|
||||
this.logger.error(`Child registration unrecognized desc: ${desc}`);
|
||||
this.logger.error(`Unrecognized softwareType: ${softwareType}`);
|
||||
}
|
||||
}
|
||||
|
||||
connectMeasurement(child, subType, position) {
|
||||
this.logger.debug(
|
||||
`Connecting measurement child: ${subType} with position=${position}`
|
||||
);
|
||||
|
||||
// Check if subType is valid
|
||||
if (!subType) {
|
||||
this.logger.error(`Invalid subType for measurement: ${subType}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// initialize the measurement to a number - logging each step for debugging
|
||||
try {
|
||||
this.logger.debug(
|
||||
`Initializing measurement: ${subType}, position: ${position} value: 0`
|
||||
);
|
||||
const typeResult = this.mainClass.measurements.type(subType);
|
||||
const variantResult = typeResult.variant("measured");
|
||||
const positionResult = variantResult.position(position);
|
||||
positionResult.value(0);
|
||||
|
||||
this.logger.debug(
|
||||
`Subscribing on mAbs event for measurement: ${subType}, position: ${position}`
|
||||
);
|
||||
// Listen for the mAbs event and update the measurement
|
||||
|
||||
this.logger.debug(
|
||||
`Successfully initialized measurement: ${subType}, position: ${position}`
|
||||
);
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to initialize measurement: ${error.message}`);
|
||||
return;
|
||||
}
|
||||
|
||||
child.emitter.on("mAbs", (value) => {
|
||||
// Use the same method chaining approach that worked during initialization
|
||||
this.mainClass.measurements
|
||||
.type(subType)
|
||||
.variant("measured")
|
||||
.position(position)
|
||||
.value(value);
|
||||
this.mainClass.updateMeasurement("measured", subType, value, position);
|
||||
//this.logger.debug(`--------->>>>>>>>>Updated measurement: ${subType}, value: ${value}, position: ${position}`);
|
||||
});
|
||||
}
|
||||
|
||||
connectMachine(machine) {
|
||||
if (!machine) {
|
||||
this.logger.error("Invalid machine provided.");
|
||||
return;
|
||||
}
|
||||
|
||||
const machineId = Object.keys(this.mainClass.machines).length + 1;
|
||||
this.mainClass.machines[machineId] = machine;
|
||||
|
||||
this.logger.info(
|
||||
`Setting up pressureChange listener for machine ${machineId}`
|
||||
);
|
||||
|
||||
machine.emitter.on("pressureChange", () =>
|
||||
this.mainClass.handlePressureChange(machine)
|
||||
);
|
||||
|
||||
//update of child triggers the handler
|
||||
this.mainClass.handleChildChange();
|
||||
|
||||
this.logger.info(`Machine ${machineId} registered successfully.`);
|
||||
}
|
||||
|
||||
connectValve(valve) {
|
||||
if (!valve) {
|
||||
this.logger.warn("Invalid valve provided.");
|
||||
return;
|
||||
}
|
||||
const valveId = Object.keys(this.mainClass.valves).length + 1;
|
||||
this.mainClass.valves[valveId] = valve; // Gooit valve object in de valves attribute met valve objects
|
||||
|
||||
valve.state.emitter.on("positionChange", (data) => {
|
||||
//ValveGroupController abboneren op klepstand verandering
|
||||
this.mainClass.logger.debug(`Position change of valve detected: ${data}`);
|
||||
this.mainClass.calcValveFlows();
|
||||
}); //bepaal nieuwe flow per valve
|
||||
valve.emitter.on("deltaPChange", () => {
|
||||
this.mainClass.logger.debug("DeltaP change of valve detected");
|
||||
this.mainClass.calcMaxDeltaP();
|
||||
}); //bepaal nieuwe max deltaP
|
||||
|
||||
this.logger.info(`Valve ${valveId} registered successfully.`);
|
||||
}
|
||||
|
||||
connectActuator(actuator, positionVsParent) {
|
||||
if (!actuator) {
|
||||
this.logger.warn("Invalid actuator provided.");
|
||||
return;
|
||||
}
|
||||
|
||||
//Special case gateGroupControl
|
||||
if (
|
||||
this.mainClass.config.functionality.softwareType == "gateGroupControl"
|
||||
) {
|
||||
if (Object.keys(this.mainClass.actuators).length < 2) {
|
||||
if (positionVsParent == "downstream") {
|
||||
this.mainClass.actuators[0] = actuator;
|
||||
}
|
||||
|
||||
if (positionVsParent == "upstream") {
|
||||
this.mainClass.actuators[1] = actuator;
|
||||
}
|
||||
//define emitters
|
||||
actuator.state.emitter.on("positionChange", (data) => {
|
||||
this.mainClass.logger.debug(`Position change of actuator detected: ${data}`);
|
||||
this.mainClass.eventUpdate();
|
||||
});
|
||||
|
||||
//define emitters
|
||||
actuator.state.emitter.on("stateChange", (data) => {
|
||||
this.mainClass.logger.debug(`State change of actuator detected: ${data}`);
|
||||
this.mainClass.eventUpdate();
|
||||
});
|
||||
|
||||
} else {
|
||||
this.logger.error(
|
||||
"Too many actuators registered. Only two are allowed."
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//wanneer hij deze ontvangt is deltaP van een van de valves veranderd (kan ook zijn niet child zijn, maar dat maakt niet uit)
|
||||
}
|
||||
|
||||
module.exports = ChildRegistrationUtils;
|
||||
94
src/helper/configUtils.js
Normal file
94
src/helper/configUtils.js
Normal file
@@ -0,0 +1,94 @@
|
||||
/**
|
||||
* @file configUtils.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
|
||||
* 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
|
||||
* prior written permission from the author.
|
||||
*
|
||||
* 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
|
||||
* SOFTWARE.
|
||||
*
|
||||
* Ownership of this code remains solely with the original author. Unauthorized
|
||||
* use of this Software is strictly prohibited.
|
||||
*
|
||||
* @summary Utility for managing and validating configuration values.
|
||||
* @description Utility for managing and validating configuration values.
|
||||
* @module ConfigUtils
|
||||
* @requires ValidationUtils
|
||||
* @requires Logger
|
||||
* @exports ConfigUtils
|
||||
* @version 0.1.0
|
||||
* @since 0.1.0
|
||||
*/
|
||||
|
||||
|
||||
const ValidationUtils = require("./validationUtils");
|
||||
const Logger = require("./logger");
|
||||
|
||||
class ConfigUtils {
|
||||
constructor(defaultConfig, IloggerEnabled , IloggerLevel) {
|
||||
const loggerEnabled = IloggerEnabled || true;
|
||||
const loggerLevel = IloggerLevel || "warn";
|
||||
this.logger = new Logger(loggerEnabled, loggerLevel, 'ConfigUtils');
|
||||
this.defaultConfig = defaultConfig;
|
||||
this.validationUtils = new ValidationUtils(loggerEnabled, loggerLevel);
|
||||
}
|
||||
|
||||
// Initialize configuration
|
||||
initConfig(config) {
|
||||
this.logger.info("Initializing configuration...");
|
||||
|
||||
// Validate the provided configuration
|
||||
const validatedConfig = this.validationUtils.validateSchema(config, this.defaultConfig, this.defaultConfig.functionality.softwareType.default);
|
||||
|
||||
this.logger.info("Configuration initialized successfully.");
|
||||
|
||||
return validatedConfig;
|
||||
}
|
||||
|
||||
|
||||
// Update configuration
|
||||
updateConfig(currentConfig, newConfig) {
|
||||
this.logger.info("Updating configuration...");
|
||||
|
||||
// Merge 2 configs and validate the result
|
||||
const mergedConfig = this.mergeObjects(currentConfig, newConfig);
|
||||
|
||||
// Merge current configuration with new values
|
||||
const updatedConfig = this.validationUtils.validateSchema(mergedConfig, this.defaultConfig, this.defaultConfig.functionality.softwareType.default);
|
||||
|
||||
this.logger.info("Configuration updated successfully.");
|
||||
return updatedConfig;
|
||||
}
|
||||
|
||||
// loop through objects and merge them obj1 will be updated with obj2 values
|
||||
mergeObjects(obj1, obj2) {
|
||||
for (let key in obj2) {
|
||||
if (obj2.hasOwnProperty(key)) {
|
||||
if (typeof obj2[key] === 'object') {
|
||||
if (!obj1[key]) {
|
||||
obj1[key] = {};
|
||||
}
|
||||
this.mergeObjects(obj1[key], obj2[key]);
|
||||
} else {
|
||||
obj1[key] = obj2[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
return obj1;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ConfigUtils;
|
||||
57
src/helper/logger.js
Normal file
57
src/helper/logger.js
Normal file
@@ -0,0 +1,57 @@
|
||||
class Logger {
|
||||
constructor(logging = true, logLevel = 'debug', nameModule = 'N/A') {
|
||||
this.logging = logging; // Boolean flag to enable/disable logging
|
||||
this.logLevel = logLevel; // Default log level: 'debug', 'info', 'warn', 'error'
|
||||
this.levels = ['debug', 'info', 'warn', 'error']; // Log levels in order of severity
|
||||
this.nameModule = nameModule; // Name of the module that uses the logger
|
||||
}
|
||||
|
||||
// Helper function to check if a log message should be displayed based on current log level
|
||||
shouldLog(level) {
|
||||
return this.levels.indexOf(level) >= this.levels.indexOf(this.logLevel);
|
||||
}
|
||||
|
||||
// Log a debug message
|
||||
debug(message) {
|
||||
if (this.logging && this.shouldLog('debug')) {
|
||||
console.debug(`[DEBUG] -> ${this.nameModule}: ${message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Log an info message
|
||||
info(message) {
|
||||
if (this.logging && this.shouldLog('info')) {
|
||||
console.info(`[INFO] -> ${this.nameModule}: ${message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Log a warning message
|
||||
warn(message) {
|
||||
if (this.logging && this.shouldLog('warn')) {
|
||||
console.warn(`[WARN] -> ${this.nameModule}: ${message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Log an error message
|
||||
error(message) {
|
||||
if (this.logging && this.shouldLog('error')) {
|
||||
console.error(`[ERROR] -> ${this.nameModule}: ${message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Set the log level dynamically
|
||||
setLogLevel(level) {
|
||||
if (this.levels.includes(level)) {
|
||||
this.logLevel = level;
|
||||
} else {
|
||||
console.error(`[ERROR ${nameModule}]: Invalid log level: ${level}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Toggle the logging on or off
|
||||
toggleLogging() {
|
||||
this.logging = !this.logging;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Logger;
|
||||
484
src/helper/menuUtils.js
Normal file
484
src/helper/menuUtils.js
Normal file
@@ -0,0 +1,484 @@
|
||||
export function initBasicToggles(elements) {
|
||||
// Toggle visibility for log level
|
||||
elements.logCheckbox.addEventListener("change", function () {
|
||||
elements.rowLogLevel.style.display = this.checked ? "block" : "none";
|
||||
});
|
||||
elements.rowLogLevel.style.display = elements.logCheckbox.checked
|
||||
? "block"
|
||||
: "none";
|
||||
}
|
||||
|
||||
// Define the initialize toggles function within scope
|
||||
export function initMeasurementToggles(elements) {
|
||||
// Toggle visibility for scaling inputs
|
||||
elements.scalingCheckbox.addEventListener("change", function () {
|
||||
elements.rowInputMin.style.display = this.checked ? "block" : "none";
|
||||
elements.rowInputMax.style.display = this.checked ? "block" : "none";
|
||||
});
|
||||
|
||||
// Set initial states
|
||||
elements.rowInputMin.style.display = elements.scalingCheckbox.checked
|
||||
? "block"
|
||||
: "none";
|
||||
elements.rowInputMax.style.display = elements.scalingCheckbox.checked
|
||||
? "block"
|
||||
: "none";
|
||||
}
|
||||
|
||||
export function initTensionToggles(elements, node) {
|
||||
const currentMethod = node.interpolationMethod;
|
||||
elements.rowTension.style.display =
|
||||
currentMethod === "monotone_cubic_spline" ? "block" : "none";
|
||||
console.log(
|
||||
"Initial tension row display: ",
|
||||
elements.rowTension.style.display
|
||||
);
|
||||
|
||||
elements.interpolationMethodInput.addEventListener("change", function () {
|
||||
const selectedMethod = this.value;
|
||||
console.log(`Interpolation method changed: ${selectedMethod}`);
|
||||
node.interpolationMethod = selectedMethod;
|
||||
|
||||
// Toggle visibility for tension input
|
||||
elements.rowTension.style.display =
|
||||
selectedMethod === "monotone_cubic_spline" ? "block" : "none";
|
||||
console.log("Tension row display: ", elements.rowTension.style.display);
|
||||
});
|
||||
}
|
||||
// Define the smoothing methods population function within scope
|
||||
export function populateSmoothingMethods(configUrls, elements, node) {
|
||||
fetchData(configUrls.cloud.config, configUrls.local.config)
|
||||
.then((configData) => {
|
||||
const smoothingMethods =
|
||||
configData.smoothing?.smoothMethod?.rules?.values?.map(
|
||||
(o) => o.value
|
||||
) || [];
|
||||
populateDropdown(
|
||||
elements.smoothMethod,
|
||||
smoothingMethods,
|
||||
node,
|
||||
"smooth_method"
|
||||
);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("Error loading smoothing methods", err);
|
||||
});
|
||||
}
|
||||
|
||||
export function populateInterpolationMethods(configUrls, elements, node) {
|
||||
fetchData(configUrls.cloud.config, configUrls.local.config)
|
||||
.then((configData) => {
|
||||
const interpolationMethods =
|
||||
configData?.interpolation?.type?.rules?.values.map((m) => m.value) ||
|
||||
[];
|
||||
populateDropdown(
|
||||
elements.interpolationMethodInput,
|
||||
interpolationMethods,
|
||||
node,
|
||||
"interpolationMethod"
|
||||
);
|
||||
|
||||
// Find the selected method and use it to spawn 1 more field to fill in tension
|
||||
//const selectedMethod = interpolationMethods.find(m => m === node.interpolationMethod);
|
||||
initTensionToggles(elements, node);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("Error loading interpolation methods", err);
|
||||
});
|
||||
}
|
||||
|
||||
export function populateLogLevelOptions(logLevelSelect, configData, node) {
|
||||
// debug log level
|
||||
//console.log("Displaying configData => ", configData) ;
|
||||
|
||||
const logLevels =
|
||||
configData?.general?.logging?.logLevel?.rules?.values?.map(
|
||||
(l) => l.value
|
||||
) || [];
|
||||
|
||||
//console.log("Displaying logLevels => ", logLevels);
|
||||
|
||||
// Reuse your existing generic populateDropdown helper
|
||||
populateDropdown(logLevelSelect, logLevels, node.logLevel);
|
||||
}
|
||||
|
||||
//cascade dropdowns for asset type, supplier, subType, model, unit
|
||||
export function fetchAndPopulateDropdowns(configUrls, elements, node) {
|
||||
|
||||
fetchData(configUrls.cloud.config, configUrls.local.config)
|
||||
.then((configData) => {
|
||||
const assetType = configData.asset?.type?.default;
|
||||
const localSuppliersUrl = constructUrl(configUrls.local.taggcodeAPI,`${assetType}s`,"suppliers.json");
|
||||
const cloudSuppliersUrl = constructCloudURL(configUrls.cloud.taggcodeAPI, "/vendor/get_vendors.php");
|
||||
|
||||
return fetchData(cloudSuppliersUrl, localSuppliersUrl)
|
||||
.then((supplierData) => {
|
||||
|
||||
const suppliers = supplierData.map((supplier) => supplier.name);
|
||||
|
||||
// Populate suppliers dropdown and set up its change handler
|
||||
return populateDropdown(
|
||||
elements.supplier,
|
||||
suppliers,
|
||||
node,
|
||||
"supplier",
|
||||
function (selectedSupplier) {
|
||||
if (selectedSupplier) {
|
||||
populateSubTypes(configUrls, elements, node, selectedSupplier);
|
||||
}
|
||||
}
|
||||
);
|
||||
})
|
||||
.then(() => {
|
||||
// If we have a saved supplier, trigger subTypes population
|
||||
if (node.supplier) {
|
||||
populateSubTypes(configUrls, elements, node, node.supplier);
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Error in initial dropdown population:", error);
|
||||
});
|
||||
}
|
||||
|
||||
export function getSpecificConfigUrl(nodeName,cloudAPI) {
|
||||
|
||||
const cloudConfigURL = cloudAPI + "/config/" + nodeName + ".json";
|
||||
const localConfigURL = "http://localhost:1880/"+ nodeName + "/dependencies/"+ nodeName + "/" + nodeName + "Config.json";
|
||||
|
||||
return { cloudConfigURL, localConfigURL };
|
||||
|
||||
}
|
||||
|
||||
// Save changes to API
|
||||
export async function apiCall(node) {
|
||||
try{
|
||||
// OLFIANT when a browser refreshes the tag code is lost!!! fix this later!!!!!
|
||||
// FIX UUID ALSO LATER
|
||||
|
||||
if(node.assetTagCode !== "" || node.assetTagCode !== null){ }
|
||||
// API call to register or check asset in central database
|
||||
let assetregisterAPI = node.configUrls.cloud.taggcodeAPI + "/asset/create_asset.php";
|
||||
|
||||
const assetModelId = node.modelMetadata.id; //asset_product_model_id
|
||||
const uuid = node.uuid; //asset_product_model_uuid
|
||||
const assetName = node.assetType; //asset_name / type?
|
||||
const description = node.name; // asset_description
|
||||
const assetStatus = "actief"; //asset_status -> koppel aan enable / disable node ? or make dropdown ?
|
||||
const assetProfileId = 1; //asset_profile_id these are the rules to check if the childs are valid under this node (parent / child id?)
|
||||
const child_assets = ["63247"]; //child_assets tagnummer of id?
|
||||
const assetProcessId = node.processId; //asset_process_id
|
||||
const assetLocationId = node.locationId; //asset_location_id
|
||||
const tagCode = node.assetTagCode; // if already exists in the node information use it to tell the api it exists and it will update else we will get it from the api call
|
||||
//console.log(`this is my tagCode: ${tagCode}`);
|
||||
|
||||
// Build base URL with required parameters
|
||||
let apiUrl = `?asset_product_model_id=${assetModelId}&asset_product_model_uuid=${uuid}&asset_name=${assetName}&asset_description=${description}&asset_status=${assetStatus}&asset_profile_id=${assetProfileId}&asset_location_id=${assetLocationId}&asset_process_id=${assetProcessId}&child_assets=${child_assets}`;
|
||||
|
||||
// Only add tagCode to URL if it exists
|
||||
if (tagCode) {
|
||||
apiUrl += `&asset_tag_number=${tagCode}`;
|
||||
console.log('hello there');
|
||||
}
|
||||
|
||||
assetregisterAPI += apiUrl;
|
||||
console.log("API call to register asset in central database", assetregisterAPI);
|
||||
|
||||
const response = await fetch(assetregisterAPI, {
|
||||
method: "POST"
|
||||
});
|
||||
|
||||
// Get the response text first
|
||||
const responseText = await response.text();
|
||||
console.log("Raw API response:", responseText);
|
||||
|
||||
// Try to parse the JSON, handling potential parsing errors
|
||||
let jsonResponse;
|
||||
try {
|
||||
jsonResponse = JSON.parse(responseText);
|
||||
} catch (parseError) {
|
||||
console.error("JSON Parsing Error:", parseError);
|
||||
console.error("Response that could not be parsed:", responseText);
|
||||
throw new Error("Failed to parse API response");
|
||||
}
|
||||
|
||||
console.log(jsonResponse);
|
||||
|
||||
if(jsonResponse.success){
|
||||
console.log(`${jsonResponse.message}, tag number: ${jsonResponse.asset_tag_number}, asset id: ${jsonResponse.asset_id}`);
|
||||
// Save the asset tag number and id to the node
|
||||
} else {
|
||||
console.log("Asset not registered in central database");
|
||||
}
|
||||
return jsonResponse;
|
||||
|
||||
} catch (error) {
|
||||
console.log("Error saving changes to asset register API", error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export async function fetchData(url, fallbackUrl) {
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
|
||||
const responsData = await response.json();
|
||||
//responsData
|
||||
const data = responsData.data;
|
||||
/* .map(item => {
|
||||
const { vendor_name, ...rest } = item;
|
||||
return {
|
||||
name: vendor_name,
|
||||
...rest
|
||||
};
|
||||
}); */
|
||||
console.log(url);
|
||||
console.log("Response Data: ", data);
|
||||
return data;
|
||||
|
||||
} catch (err) {
|
||||
console.warn(
|
||||
`Primary URL failed: ${url}. Trying fallback URL: ${fallbackUrl}`,
|
||||
err
|
||||
);
|
||||
try {
|
||||
const response = await fetch(fallbackUrl);
|
||||
if (!response.ok)
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
return await response.json();
|
||||
} catch (fallbackErr) {
|
||||
console.error("Both primary and fallback URLs failed:", fallbackErr);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchProjectData(url) {
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
|
||||
const responsData = await response.json();
|
||||
console.log("Response Data: ", responsData);
|
||||
return responsData;
|
||||
|
||||
} catch (err) {
|
||||
}
|
||||
}
|
||||
|
||||
async function populateDropdown(
|
||||
htmlElement,
|
||||
options,
|
||||
node,
|
||||
property,
|
||||
callback
|
||||
) {
|
||||
generateHtml(htmlElement, options, node[property]);
|
||||
|
||||
htmlElement.addEventListener("change", async (e) => {
|
||||
const newValue = e.target.value;
|
||||
console.log(`Dropdown changed: ${property} = ${newValue}`);
|
||||
node[property] = newValue;
|
||||
|
||||
RED.nodes.dirty(true);
|
||||
if (callback) await callback(newValue); // Ensure async callback completion
|
||||
});
|
||||
}
|
||||
|
||||
// Helper function to construct a URL from a base and path internal
|
||||
function constructUrl(base, ...paths) {
|
||||
|
||||
// Remove trailing slash from base and leading slashes from paths
|
||||
const sanitizedBase = (base || "").replace(/\/+$/, "");
|
||||
const sanitizedPaths = paths.map((path) => path.replace(/^\/+|\/+$/g, ""));
|
||||
|
||||
// Join sanitized base and paths
|
||||
const url = `${sanitizedBase}/${sanitizedPaths.join("/")}`;
|
||||
console.log("Base:", sanitizedBase);
|
||||
console.log("Paths:", sanitizedPaths);
|
||||
console.log("Constructed URL:", url);
|
||||
return url;
|
||||
}
|
||||
|
||||
//Adjust for API Gateway
|
||||
function constructCloudURL(base, ...paths) {
|
||||
// Remove trailing slash from base and leading slashes from paths
|
||||
const sanitizedBase = base.replace(/\/+$/, "");
|
||||
const sanitizedPaths = paths.map((path) => path.replace(/^\/+|\/+$/g, ""));
|
||||
// Join sanitized base and paths
|
||||
const url = `${sanitizedBase}/${sanitizedPaths.join("/")}`;
|
||||
return url;
|
||||
}
|
||||
|
||||
function populateSubTypes(configUrls, elements, node, selectedSupplier) {
|
||||
|
||||
fetchData(configUrls.cloud.config, configUrls.local.config)
|
||||
.then((configData) => {
|
||||
const assetType = configData.asset?.type?.default;
|
||||
const supplierFolder = constructUrl( configUrls.local.taggcodeAPI, `${assetType}s`, selectedSupplier );
|
||||
|
||||
const localSubTypesUrl = constructUrl(supplierFolder, "subtypes.json");
|
||||
const cloudSubTypesUrl = constructCloudURL(configUrls.cloud.taggcodeAPI, "/product/get_subtypesFromVendor.php?vendor_name=" + selectedSupplier);
|
||||
|
||||
return fetchData(cloudSubTypesUrl, localSubTypesUrl)
|
||||
.then((subTypeData) => {
|
||||
const subTypes = subTypeData.map((subType) => subType.name);
|
||||
|
||||
return populateDropdown(
|
||||
elements.subType,
|
||||
subTypes,
|
||||
node,
|
||||
"subType",
|
||||
function (selectedSubType) {
|
||||
if (selectedSubType) {
|
||||
// When subType changes, update both models and units
|
||||
populateModels(
|
||||
configUrls,
|
||||
elements,
|
||||
node,
|
||||
selectedSupplier,
|
||||
selectedSubType
|
||||
);
|
||||
populateUnitsForSubType(
|
||||
configUrls,
|
||||
elements,
|
||||
node,
|
||||
selectedSubType
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
})
|
||||
.then(() => {
|
||||
// If we have a saved subType, trigger both models and units population
|
||||
if (node.subType) {
|
||||
populateModels(
|
||||
configUrls,
|
||||
elements,
|
||||
node,
|
||||
selectedSupplier,
|
||||
node.subType
|
||||
);
|
||||
populateUnitsForSubType(configUrls, elements, node, node.subType);
|
||||
}
|
||||
//console.log("In fetch part of subtypes ");
|
||||
// Store all data from selected model
|
||||
/* node["modelMetadata"] = modelData.find(
|
||||
(model) => model.name === node.model
|
||||
);
|
||||
console.log("Model Metadata: ", node["modelMetadata"]); */
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Error populating subtypes:", error);
|
||||
});
|
||||
}
|
||||
|
||||
function populateUnitsForSubType(configUrls, elements, node, selectedSubType) {
|
||||
// Fetch the units data
|
||||
fetchData(configUrls.cloud.units, configUrls.local.units)
|
||||
.then((unitsData) => {
|
||||
// Find the category that matches the subType name
|
||||
const categoryData = unitsData.units.find(
|
||||
(category) =>
|
||||
category.category.toLowerCase() === selectedSubType.toLowerCase()
|
||||
);
|
||||
|
||||
if (categoryData) {
|
||||
// Extract just the unit values and descriptions
|
||||
const units = categoryData.values.map((unit) => ({
|
||||
value: unit.value,
|
||||
description: unit.description,
|
||||
}));
|
||||
|
||||
// Create the options array with descriptions as labels
|
||||
const options = units.map((unit) => ({
|
||||
value: unit.value,
|
||||
label: `${unit.value} - ${unit.description}`,
|
||||
}));
|
||||
|
||||
// Populate the units dropdown
|
||||
populateDropdown(
|
||||
elements.unit,
|
||||
options.map((opt) => opt.value),
|
||||
node,
|
||||
"unit"
|
||||
);
|
||||
|
||||
// If there's no currently selected unit but we have options, select the first one
|
||||
if (!node.unit && options.length > 0) {
|
||||
node.unit = options[0].value;
|
||||
elements.unit.value = options[0].value;
|
||||
}
|
||||
} else {
|
||||
// If no matching category is found, provide a default % option
|
||||
const defaultUnits = [{ value: "%", description: "Percentage" }];
|
||||
populateDropdown(
|
||||
elements.unit,
|
||||
defaultUnits.map((unit) => unit.value),
|
||||
node,
|
||||
"unit"
|
||||
);
|
||||
console.warn(
|
||||
`No matching unit category found for subType: ${selectedSubType}`
|
||||
);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Error fetching units:", error);
|
||||
});
|
||||
}
|
||||
|
||||
function populateModels(
|
||||
configUrls,
|
||||
elements,
|
||||
node,
|
||||
selectedSupplier,
|
||||
selectedSubType
|
||||
) {
|
||||
|
||||
fetchData(configUrls.cloud.config, configUrls.local.config)
|
||||
.then((configData) => {
|
||||
const assetType = configData.asset?.type?.default;
|
||||
// save assetType to fetch later
|
||||
node.assetType = assetType;
|
||||
|
||||
const supplierFolder = constructUrl( configUrls.local.taggcodeAPI,`${assetType}s`,selectedSupplier);
|
||||
const subTypeFolder = constructUrl(supplierFolder, selectedSubType);
|
||||
const localModelsUrl = constructUrl(subTypeFolder, "models.json");
|
||||
const cloudModelsUrl = constructCloudURL(configUrls.cloud.taggcodeAPI, "/product/get_product_models.php?vendor_name=" + selectedSupplier + "&product_subtype_name=" + selectedSubType);
|
||||
|
||||
return fetchData(cloudModelsUrl, localModelsUrl).then((modelData) => {
|
||||
const models = modelData.map((model) => model.name); // use this to populate the dropdown
|
||||
|
||||
// If a model is already selected, store its metadata immediately
|
||||
if (node.model) {
|
||||
node["modelMetadata"] = modelData.find((model) => model.name === node.model);
|
||||
}
|
||||
|
||||
populateDropdown(elements.model, models, node, "model", (selectedModel) => {
|
||||
// Store only the metadata for the selected model
|
||||
node["modelMetadata"] = modelData.find((model) => model.name === selectedModel);
|
||||
});
|
||||
/*
|
||||
console.log('hello here I am:');
|
||||
console.log(node["modelMetadata"]);
|
||||
*/
|
||||
});
|
||||
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Error populating models:", error);
|
||||
});
|
||||
}
|
||||
|
||||
function generateHtml(htmlElement, options, savedValue) {
|
||||
htmlElement.innerHTML = options.length
|
||||
? `<option value="">Select...</option>${options
|
||||
.map((opt) => `<option value="${opt}">${opt}</option>`)
|
||||
.join("")}`
|
||||
: "<option value=''>No options available</option>";
|
||||
|
||||
if (savedValue && options.includes(savedValue)) {
|
||||
htmlElement.value = savedValue;
|
||||
}
|
||||
}
|
||||
56
src/helper/nodeTemplates.js
Normal file
56
src/helper/nodeTemplates.js
Normal file
@@ -0,0 +1,56 @@
|
||||
const nodeTemplates = {
|
||||
asset: {
|
||||
category: "digital asset",
|
||||
color: "#4f8582",
|
||||
defaults: {
|
||||
name: { value: "", required: true },
|
||||
enableLog: { value: false },
|
||||
logLevel: { value: "error" },
|
||||
parent: { value: "downstream" }, // indicates the position vs the parent in the process downstream,upstream or none.
|
||||
supplier: { value: "" },
|
||||
subType: { value: "" },
|
||||
model: { value: "" },
|
||||
unit: { value: "" },
|
||||
},
|
||||
inputs: 1,
|
||||
outputs: 3,
|
||||
inputLabels: ["Machine Input"],
|
||||
outputLabels: ["process", "dbase", "parent"],
|
||||
icon: "font-awesome/fa-cogs",
|
||||
elements: {
|
||||
// Basic fields
|
||||
name: "node-input-name",
|
||||
// Logging fields
|
||||
logCheckbox: "node-input-enableLog",
|
||||
logLevelSelect: "node-input-logLevel",
|
||||
rowLogLevel: "row-logLevel",
|
||||
// Asset fields
|
||||
supplier: "node-input-supplier",
|
||||
subType: "node-input-subType",
|
||||
model: "node-input-model",
|
||||
unit: "node-input-unit",
|
||||
//position vs parent
|
||||
parent: "node-input-parent",
|
||||
},
|
||||
projectSettingsURL:
|
||||
"http://localhost:1880/generalFunctions/settings/projectSettings.json",
|
||||
},
|
||||
|
||||
exampleTemplate: {
|
||||
category: "digital twin",
|
||||
color: "#004080",
|
||||
defaults: {
|
||||
name: { value: "", required: true },
|
||||
foo: { value: 42 },
|
||||
},
|
||||
inputs: 2,
|
||||
outputs: 2,
|
||||
inputLabels: ["In A", "In B"],
|
||||
outputLabels: ["Out A", "Out B"],
|
||||
icon: "font-awesome/fa-gears",
|
||||
},
|
||||
|
||||
// …add more node “templates” here…
|
||||
};
|
||||
|
||||
export default nodeTemplates;
|
||||
132
src/helper/outputUtils.js
Normal file
132
src/helper/outputUtils.js
Normal file
@@ -0,0 +1,132 @@
|
||||
//this class will handle the output events for the node red node
|
||||
class OutputUtils {
|
||||
constructor() {
|
||||
this.output ={};
|
||||
this.output['influxdb'] = {};
|
||||
this.output['process'] = {};
|
||||
}
|
||||
|
||||
checkForChanges(output, format) {
|
||||
const changedFields = {};
|
||||
for (const key in output) {
|
||||
if (output.hasOwnProperty(key) && output[key] !== this.output[format][key]) {
|
||||
let value = output[key];
|
||||
// For fields: if the value is an object (and not a Date), stringify it.
|
||||
if (value !== null && typeof value === 'object' && !(value instanceof Date)) {
|
||||
changedFields[key] = JSON.stringify(value);
|
||||
} else {
|
||||
changedFields[key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update the saved output state.
|
||||
this.output[format] = { ...this.output[format], ...changedFields };
|
||||
|
||||
return changedFields;
|
||||
}
|
||||
|
||||
formatMsg(output, config, format) {
|
||||
|
||||
//define emtpy message
|
||||
let msg = {};
|
||||
|
||||
// Compare output with last output and only include changed values
|
||||
const changedFields = this.checkForChanges(output,format);
|
||||
|
||||
if (Object.keys(changedFields).length > 0) {
|
||||
|
||||
switch (format) {
|
||||
case 'influxdb':
|
||||
// Extract the relevant config properties.
|
||||
const relevantConfig = this.extractRelevantConfig(config);
|
||||
// Flatten the tags so that no nested objects are passed on.
|
||||
const flatTags = this.flattenTags(relevantConfig);
|
||||
msg = this.influxDBFormat(changedFields, config, flatTags);
|
||||
|
||||
break;
|
||||
|
||||
case 'process':
|
||||
|
||||
// Compare output with last output and only include changed values
|
||||
msg = this.processFormat(changedFields,config);
|
||||
//console.log(msg);
|
||||
break;
|
||||
|
||||
default:
|
||||
console.log('Unknown format in output utils');
|
||||
break;
|
||||
}
|
||||
return msg;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
influxDBFormat(changedFields, config , flatTags) {
|
||||
// Create the measurement and topic using softwareType and name config.functionality.softwareType + .
|
||||
const measurement = config.general.name;
|
||||
const payload = {
|
||||
measurement: measurement,
|
||||
fields: changedFields,
|
||||
tags: flatTags,
|
||||
timestamp: new Date(),
|
||||
};
|
||||
|
||||
const topic = measurement;
|
||||
const msg = { topic: topic, payload: payload };
|
||||
return msg;
|
||||
}
|
||||
|
||||
flattenTags(obj) {
|
||||
const result = {};
|
||||
for (const key in obj) {
|
||||
if (obj.hasOwnProperty(key)) {
|
||||
const value = obj[key];
|
||||
if (value !== null && typeof value === 'object' && !(value instanceof Date)) {
|
||||
// Recursively flatten the nested object.
|
||||
const flatChild = this.flattenTags(value);
|
||||
for (const childKey in flatChild) {
|
||||
if (flatChild.hasOwnProperty(childKey)) {
|
||||
result[`${key}_${childKey}`] = String(flatChild[childKey]);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// InfluxDB tags must be strings.
|
||||
result[key] = String(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
extractRelevantConfig(config) {
|
||||
|
||||
return {
|
||||
// general properties
|
||||
id: config.general?.id,
|
||||
name: config.general?.name,
|
||||
unit: config.general?.unit,
|
||||
// functionality properties
|
||||
softwareType: config.functionality?.softwareType,
|
||||
role: config.functionality?.role,
|
||||
// asset properties (exclude machineCurve)
|
||||
uuid: config.asset?.uuid,
|
||||
geoLocation: config.asset?.geoLocation,
|
||||
supplier: config.asset?.supplier,
|
||||
type: config.asset?.type,
|
||||
subType: config.asset?.subType,
|
||||
model: config.asset?.model,
|
||||
};
|
||||
}
|
||||
|
||||
processFormat(changedFields,config) {
|
||||
// Create the measurement and topic using softwareType and name config.functionality.softwareType + .
|
||||
const measurement = config.general.name;
|
||||
const payload = changedFields;
|
||||
const topic = measurement;
|
||||
const msg = { topic: topic, payload: payload };
|
||||
return msg;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = OutputUtils;
|
||||
528
src/helper/validationUtils.js
Normal file
528
src/helper/validationUtils.js
Normal file
@@ -0,0 +1,528 @@
|
||||
/**
|
||||
* @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
|
||||
* 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
|
||||
* prior written permission from the author.
|
||||
*
|
||||
* 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
|
||||
* SOFTWARE.
|
||||
*
|
||||
* 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
|
||||
* @since 0.1.0
|
||||
*/
|
||||
|
||||
const Logger = require("./logger");
|
||||
|
||||
class ValidationUtils {
|
||||
constructor(IloggerEnabled, IloggerLevel) {
|
||||
const loggerEnabled = IloggerEnabled || true;
|
||||
const loggerLevel = IloggerLevel || "warn";
|
||||
this.logger = new Logger(loggerEnabled, loggerLevel, 'ValidationUtils');
|
||||
}
|
||||
|
||||
constrain(value, min, max) {
|
||||
if (typeof value !== "number") {
|
||||
this.logger?.warn(`Value '${value}' is not a number. Defaulting to ${min}.`);
|
||||
return min;
|
||||
}
|
||||
return Math.min(Math.max(value, min), max);
|
||||
}
|
||||
|
||||
validateSchema(config, schema, name) {
|
||||
|
||||
const validatedConfig = {};
|
||||
let configValue;
|
||||
|
||||
// 1. Remove any unknown keys (keys not defined in the schema).
|
||||
// Log a warning and omit them from the final config.
|
||||
for (const key of Object.keys(config)) {
|
||||
if (!(key in schema)) {
|
||||
this.logger.warn(
|
||||
`[${name}] Unknown key '${key}' found in config. Removing it.`
|
||||
);
|
||||
delete config[key];
|
||||
}
|
||||
}
|
||||
|
||||
// Validate each key in the schema and loop over wildcards if they are not in schema
|
||||
for ( const key in schema ) {
|
||||
|
||||
if (key === "rules" || key === "description" || key === "schema") {
|
||||
continue;
|
||||
}
|
||||
|
||||
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) {
|
||||
// If there's a nested schema, go deeper with an empty object rather than logging "no rule"
|
||||
if (rules.schema) {
|
||||
this.logger.warn(`${name}.${key} has no default, but has a nested schema.`);
|
||||
validatedConfig[key] = this.validateSchema({}, rules.schema, `${name}.${key}`);
|
||||
}
|
||||
else {
|
||||
this.logger.info(
|
||||
`There is no rule for ${name}.${key} and no default value. ` +
|
||||
`Using full schema value but validating deeper levels first...`
|
||||
);
|
||||
|
||||
const SubObject = this.validateSchema({}, fieldSchema, `${name}.${key}`);
|
||||
|
||||
validatedConfig[key] = SubObject;
|
||||
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
this.logger.info(`There is no value provided for ${name}.${key}. Using default value.`);
|
||||
configValue = fieldSchema.default;
|
||||
}
|
||||
//continue;
|
||||
} else {
|
||||
// Use the provided value if it exists, otherwise use the default value
|
||||
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;
|
||||
}
|
||||
|
||||
// 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) {
|
||||
|
||||
if (Array.isArray(obj)) {
|
||||
return obj.map((item) => this.removeUnwantedKeys(item));
|
||||
}
|
||||
if (obj && typeof obj === "object") {
|
||||
const newObj = {};
|
||||
for (const [k, v] of Object.entries(obj)) {
|
||||
|
||||
// Skip or remove keys like 'default', 'rules', 'description', etc.
|
||||
if (["rules", "description"].includes(k)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if("default" in v){
|
||||
//put the default value in the object
|
||||
newObj[k] = v.default;
|
||||
continue;
|
||||
}
|
||||
|
||||
newObj[k] = this.removeUnwantedKeys(v);
|
||||
}
|
||||
return newObj;
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
return newConfigValue;
|
||||
}
|
||||
|
||||
validateSet(configValue, rules, fieldSchema, name, key) {
|
||||
// 1. Ensure we have a Set. If not, use default.
|
||||
if (!(configValue instanceof Set)) {
|
||||
this.logger.info(`${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.
|
||||
if (validatedArray.length < (rules.minLength || 1)) {
|
||||
this.logger.warn(
|
||||
`${name}.${key} contains fewer items than allowed (${rules.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.info(`${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);
|
||||
|
||||
if (validatedArray.length < (rules.minLength || 1)) {
|
||||
this.logger.warn(
|
||||
`${name}.${key} contains fewer items than allowed (${rules.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;
|
||||
}
|
||||
|
||||
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)) {
|
||||
|
||||
this.logger.debug(`${name}.${key} has no defined rules but is an object going 1 level deeper.`);
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ValidationUtils;
|
||||
Reference in New Issue
Block a user