const { matches } = require("lodash");
|
const fs = require("fs");
|
const os = require("os");
|
const path = require("path");
|
|
class LayoutConfigReaderServiceClass {
|
errors = [];
|
|
performUpdateAction = (target_, source_, key_, removeAction_, defaultAction_, addAction_, prependAction_) => {
|
const target = target_;
|
let result = undefined;
|
if (removeAction_ != undefined && source_["-" + key_] != undefined) {
|
result = removeAction_(target, source_, key_);
|
}
|
if (defaultAction_ != undefined && source_[key_] != undefined) {
|
if (result != undefined) target[key_] = result;
|
result = defaultAction_(target, source_, key_);
|
}
|
if (addAction_ != undefined && source_["+" + key_] != undefined) {
|
if (result != undefined) target[key_] = result;
|
result = addAction_(target, source_, key_);
|
}
|
if (prependAction_ != undefined && source_["<" + key_] != undefined) {
|
if (result != undefined) target[key_] = result;
|
result = prependAction_(target, source_, key_);
|
}
|
return result; // result == undefined -> no source_ key_ found!
|
}; // returns: result | undefined
|
|
arrayOfObjectsSetToAllNonExistingInSource = (target, source, key, objectIdentifier) => {
|
const srcKey = "-" + key;
|
const result = { ...target };
|
result[key] = [];
|
for (const entry of target[key]) {
|
if (source[srcKey].findIndex((value) => value[objectIdentifier] == entry[objectIdentifier]) < 0) {
|
result[key].push(entry);
|
}
|
}
|
return result;
|
};
|
onlyUseSourceValue = (target, source, key) => {
|
const result = target;
|
result[key] = source[key];
|
return result;
|
};
|
arrayPushSourceValuesToTargetValues = (target, source, key) => {
|
const result = target;
|
let sourceKey = key;
|
if (source["+" + sourceKey] != undefined) {
|
sourceKey = "+" + sourceKey;
|
}
|
if (source[sourceKey] != undefined) {
|
if (result[key] == undefined) {
|
result[key] = [];
|
}
|
result[key].push(...source[sourceKey]);
|
}
|
return result;
|
};
|
arrayPrependSourceValuesToTargetValues = (target, source, key) => {
|
const result = target;
|
let sourceKey = key;
|
if (source["<" + sourceKey] != undefined) {
|
sourceKey = "<" + sourceKey;
|
}
|
if (source[sourceKey] != undefined) {
|
if (result[key] == undefined) {
|
result[key] = [];
|
}
|
result[key].unshift(...source[sourceKey]);
|
}
|
return result;
|
};
|
arrayOnlyTakeValuesThatAreNotInSource = (target, source, key, sourcePrefix) => {
|
const resultArray = target;
|
const sourceKey = sourcePrefix + key;
|
if (source[sourceKey] != undefined) {
|
if (resultArray[key] == undefined) {
|
resultArray[key] = [];
|
}
|
source[sourceKey].forEach(item => {
|
const index = resultArray[key].indexOf(item);
|
if (index !== -1) {
|
resultArray[key].splice(index, 1);
|
}
|
});
|
}
|
return resultArray;
|
};
|
arrayAddOrOverwriteValuesInTarget = (target, source, key, objectIdentifier, updateAction) => {
|
const valuesToAdd = [];
|
const valuesToUpdate = [];
|
const srcKey = "+" + key;
|
for (const sourceEntry of source[srcKey]) {
|
const targetEntry = target[key].find((targetEntry) => {
|
return targetEntry[objectIdentifier] == sourceEntry[objectIdentifier];
|
});
|
|
if (targetEntry != undefined) {
|
valuesToUpdate.push({ old: targetEntry, new: sourceEntry });
|
}
|
else {
|
valuesToAdd.push(sourceEntry);
|
}
|
}
|
|
|
const result = [];
|
for (const targetEntry of target[key]) { // push unchanged target entries
|
const updateEntry = valuesToUpdate.find((value) => {
|
return value.old[objectIdentifier] == targetEntry[objectIdentifier];
|
});
|
if (updateEntry == undefined) {
|
result.push(targetEntry);
|
}
|
}
|
result.push(...valuesToAdd);// push new source entries
|
for (const toUpdate of valuesToUpdate) { // update target entries from source
|
result.push(updateAction(toUpdate.old, toUpdate.new));
|
}
|
|
const resultJson = target;
|
resultJson[key] = result;
|
return resultJson;
|
};
|
arrayPrependOrOverwriteValuesInTarget = (target, source, key, objectIdentifier, updateAction) => {
|
const valuesToPrepend = [];
|
const valuesToUpdate = [];
|
|
for (const sourceEntry of source["<" + key]) {
|
const targetEntry = target[key].find((targetEntry) => {
|
return targetEntry[objectIdentifier] == sourceEntry[objectIdentifier];
|
});
|
|
if (targetEntry != undefined) {
|
valuesToUpdate.push({ old: targetEntry, new: sourceEntry });
|
}
|
else {
|
valuesToPrepend.push(sourceEntry);
|
}
|
}
|
|
const result = [];
|
result.push(...valuesToPrepend);// prepend new source entries
|
for (const targetEntry of target[key]) { // push unchanged target entries
|
const updateEntry = valuesToUpdate.find((value) => {
|
return value.old[objectIdentifier] == targetEntry[objectIdentifier];
|
});
|
if (updateEntry == undefined) {
|
result.push(targetEntry);
|
}
|
}
|
for (const toUpdate of valuesToUpdate) { // update target entries from source
|
result.push(updateAction(toUpdate.old, toUpdate.new));
|
}
|
|
|
const resultJson = target;
|
resultJson[key] = result;
|
return resultJson;
|
};
|
|
|
objectOnlyTakeValueKeysThatAreNotInSource = (target, source, key) => {
|
const srcKey = "-" + key;
|
if (source == undefined || source[srcKey] == undefined) return target;
|
|
const targetValueKeys = Object.keys(target[key]);
|
const sourceValueKeys = Object.keys(source[srcKey]);
|
if (targetValueKeys.length < 1 && sourceValueKeys.length < 1) return undefined;
|
|
const resultObject = target;
|
if (resultObject[key] == undefined) {
|
return target;
|
}
|
for (const sourceKey of sourceValueKeys) {
|
delete resultObject[key][sourceKey];
|
}
|
return resultObject;
|
};
|
objectPushSourceValuesToTarget = (target, source, key) => {
|
const srcKey = "+" + key;
|
if (source == undefined || source[srcKey] == undefined) return target;
|
|
const targetKeys = Object.keys(target?.[key] || []);
|
const sourceKeys = Object.keys(source?.[srcKey] || []);
|
if (sourceKeys.length < 1 && targetKeys.length < 1) return undefined;
|
|
const result = target;
|
if (result[key] == undefined) {
|
result[key] = {};
|
}
|
for (const sourceKey of sourceKeys) {
|
result[key][sourceKey] = source[srcKey][sourceKey];
|
}
|
return result;
|
};
|
objectPrependSourceValuesToTarget = (target, source, key) => {
|
const srcKey = "<" + key;
|
if (source == undefined || source[srcKey] == undefined) return target;
|
|
const targetKeys = Object.keys(target[key]);
|
const sourceKeys = Object.keys(source[srcKey]);
|
if (sourceKeys.length < 1 && targetKeys.length < 1) return undefined;
|
|
const result = source;// prepend
|
|
if (result[key] == undefined) {
|
result[key] = {};
|
}
|
result[key] = source[srcKey];
|
delete result[srcKey];
|
|
for (const targetKey of targetKeys) {// restore target Values after sourceValues
|
if (result[key][targetKey] == undefined) {
|
result[key][targetKey] = target[key][targetKey];
|
}
|
}
|
return result;
|
};
|
defaultRemove = (target, key) => {
|
const result = target;
|
result[key] = undefined;
|
return result;
|
};
|
|
|
updateValue = (target, source, key) => {
|
const result = this.performUpdateAction(target, source, key, (target_, source_, key_) => {
|
//remove
|
return this.defaultRemove(target_, key_);
|
}, (target_, source_, key_) => {
|
//default
|
return this.onlyUseSourceValue(target_, source_, key_);
|
}, undefined, undefined);
|
|
if (result == undefined) {
|
return target;
|
}
|
return result;// unchanged
|
};
|
updateExecutables = (target, source) => {
|
const result = this.performUpdateAction(target, source, "execute", (target_, source_, key_) => {
|
// remove
|
return this.arrayOfObjectsSetToAllNonExistingInSource(target_, source_, key_, "name");
|
}, (target_, source_, key_) => {
|
// default
|
return this.onlyUseSourceValue(target_, source_, key_);
|
}, (target_, source_, key_) => {
|
// add
|
return this.arrayPushSourceValuesToTargetValues(target_, source_, key_);
|
}, (target_, source_, key_) => {
|
//prepend
|
return this.arrayPrependSourceValuesToTargetValues(target_, source_, key_);
|
});
|
if (result == undefined) return target;
|
return result;
|
};
|
|
|
checkSubIdentifierMatches = (value, source, subIdentifier) => {
|
let matchCounter = 0;
|
const valueIdentifierValue = value[subIdentifier];
|
const sourceIdentifierValue = source[subIdentifier];
|
if (sourceIdentifierValue == undefined && valueIdentifierValue == undefined) return ++matchCounter;
|
if (sourceIdentifierValue == undefined || valueIdentifierValue == undefined) return matchCounter;
|
if (Array.isArray(valueIdentifierValue) != Array.isArray(sourceIdentifierValue) || typeof valueIdentifierValue != typeof sourceIdentifierValue) return matchCounter;
|
if (Array.isArray(valueIdentifierValue) && Array.isArray(sourceIdentifierValue)) {
|
const intersection = valueIdentifierValue.filter(element => sourceIdentifierValue.includes(element));
|
// console.log("subidentifier:", subIdentifier, "value:", value, "source:", source, "intersection:", intersection);
|
matchCounter += intersection.length;
|
return matchCounter;
|
}
|
if (valueIdentifierValue == sourceIdentifierValue) ++matchCounter;
|
return matchCounter;
|
};
|
updateModuleAreaOrChildrenObjectArray = (targetArray, sourceArray, uniqueIdentifier, subIdentifiers) => {
|
const entriesToAdd = [];
|
const entriesToUpdate = [];
|
for (const source of sourceArray) {
|
let differingMatchCounters = false;
|
let lastMatchCount = -1;
|
const objectMatchCounterList = [];
|
for (const target of targetArray) {
|
if (target[uniqueIdentifier] == source[uniqueIdentifier]) {
|
let matches = 1;
|
for (const identifier of subIdentifiers) {
|
matches += this.checkSubIdentifierMatches(target, source, identifier);
|
}
|
if (lastMatchCount != -1 && lastMatchCount != matches) {
|
differingMatchCounters = true;
|
}
|
lastMatchCount = matches;
|
objectMatchCounterList.push({ target: target, matches: matches });
|
}
|
}
|
|
if (objectMatchCounterList.length < 1) {
|
entriesToAdd.push(source);
|
continue;
|
}
|
|
if (differingMatchCounters) {
|
objectMatchCounterList.sort((a, b) => {
|
return b.matches - a.matches;
|
});
|
}
|
|
entriesToUpdate.push({ old: objectMatchCounterList[0], new: source, allMatches: objectMatchCounterList });
|
}
|
|
|
const result = [];
|
for (const target of targetArray) {
|
//find target int entriesToUpdate old
|
let includes = false;
|
for (const entryToUpdate of entriesToUpdate) {
|
if (entryToUpdate.old.target == target) {
|
includes = true;
|
}
|
}
|
|
if (!includes) {
|
result.push(target);
|
}
|
}
|
|
result.push(...entriesToAdd);
|
for (const toUpdate of entriesToUpdate) {
|
const toPush = this.updateModuleOrArea(toUpdate.old.target, toUpdate.new);
|
|
if (Array.isArray(toPush)) {
|
let hasEqualMatchCounts = false;
|
for (const match of toUpdate.allMatches) {
|
for (const otherMatch of toUpdate.allMatches) {
|
if (match != otherMatch) {
|
if (match.matches == otherMatch.matches) {
|
hasEqualMatchCounts = true;
|
}
|
}
|
}
|
}
|
|
if (hasEqualMatchCounts) {
|
this.errors.push({
|
"pre-text": "Error! No single target could be determined for:",
|
"overwriting-object": toUpdate.new,
|
"post-text": " multiple valid targets where found."
|
});
|
console.error("LayoutConfigReaderService> Error! No single target could be determined for:", toUpdate.new, "multiple valid targets where found.");
|
continue;
|
}
|
}
|
|
result.push(toPush);
|
}
|
return result;
|
};
|
updateModuleAreaOrChildren = (target, source, key, uniqueIdentifier, subIdentifiers) => {
|
return this.performUpdateAction(target, source, key, (target_, source_, key_) => {
|
// remove
|
return this.defaultRemove(target_, key_);
|
}, (target_, source_, key_) => {
|
// default
|
const result = target_;
|
if (target[key_] == undefined && source_[key_] != undefined) {
|
result[key_] = source_[key_];
|
}
|
else if (target[key_] != undefined && source_[key_] != undefined) {
|
result[key_] = this.updateModuleAreaOrChildrenObjectArray(target_[key_], source_[key_], uniqueIdentifier, subIdentifiers);
|
}
|
return result;
|
}, this.arrayPushSourceValuesToTargetValues, this.arrayPrependSourceValuesToTargetValues);
|
};
|
updateCustomCSS = (target, source) => {
|
const result = this.performUpdateAction(target, source, "customCSS", (target_, source_, key_) => {
|
return this.arrayOnlyTakeValuesThatAreNotInSource(target_, source_, key_, "-");
|
}, (target_, source_, key_) => {
|
return this.onlyUseSourceValue(target_, source_, key_);
|
}, (target_, source_, key_) => {
|
const result = this.arrayPushSourceValuesToTargetValues(target_, source_, key_);
|
return result;
|
}, (target_, source_, key_) => {
|
return this.arrayPrependSourceValuesToTargetValues(target_, source_, key_);
|
});
|
if (result == undefined) {
|
return target;
|
}
|
return result;
|
};
|
updateClasses = (target, source) => {
|
const result = this.performUpdateAction(target, source, "classes", (target_, source_, key_) => {
|
return this.arrayOnlyTakeValuesThatAreNotInSource(target_, source_, key_, "-");
|
}, (target_, source_, key_) => {
|
return this.onlyUseSourceValue(target_, source_, key_);
|
}, (target_, source_, key_) => {
|
const result = this.arrayPushSourceValuesToTargetValues(target_, source_, key_);
|
return result;
|
}, (target_, source_, key_) => {
|
return this.arrayPrependSourceValuesToTargetValues(target_, source_, key_);
|
});
|
if (result == undefined) {
|
return target;
|
}
|
return result;
|
};
|
updateStyleVars = (target, source) => {
|
const result = this.performUpdateAction(target, source, "styleVars", this.objectOnlyTakeValueKeysThatAreNotInSource, this.onlyUseSourceValue, this.objectPushSourceValuesToTarget, this.objectPrependSourceValuesToTarget);
|
if (result == undefined) {
|
return target;
|
}
|
return result;
|
};
|
updateProperties = (target, source) => {
|
const result = this.performUpdateAction(target, source, "properties", (target_, source_, key_) => {
|
// remove
|
return this.arrayOfObjectsSetToAllNonExistingInSource(target_, source_, key_, "key");
|
}, this.onlyUseSourceValue, (target, source, key) => {
|
// add
|
return this.arrayAddOrOverwriteValuesInTarget(target, source, key, "key", (oldValue, newValue) => {
|
return newValue;
|
});
|
}, (target, source, key) => {
|
// prepend
|
return this.arrayPrependOrOverwriteValuesInTarget(target, source, key, "key", (oldValue, newValue) => {
|
return newValue;
|
});
|
});
|
if (result == undefined) {
|
return target;
|
}
|
return result;
|
};
|
updateEvents = (target, source) => {
|
const result = this.performUpdateAction(target, source, "events", (target_, source_, key_) => {
|
// remove
|
return this.arrayOfObjectsSetToAllNonExistingInSource(target_, source_, key_, "id");
|
}, this.onlyUseSourceValue, this.arrayPushSourceValuesToTargetValues, this.arrayPrependSourceValuesToTargetValues);
|
if (result == undefined) return target;
|
return result;
|
};
|
|
|
updateModuleOrArea = (target, source) => {
|
if (target.tag != source.tag) return target;
|
if (target.query != source.query) return target;
|
|
let result = target;
|
result = this.updateValue(result, source, "prepend");
|
if (Array.isArray(result)) console.log("result is Array! Post updateValue prepend");
|
result = this.updateExecutables(result, source);
|
if (Array.isArray(result)) console.log("result is Array! Post updateExecutables");
|
if (source.modules || source["-modules"] || source["+modules"] || source["<modules"]) {
|
result = this.updateModuleAreaOrChildren(result, source, "modules", "tag", ["index", "classes"]);
|
}
|
if (Array.isArray(result)) console.log("result is Array! Post modules");
|
if (source.areas || source["-areas"] || source["+areas"] || source["<areas"]) {
|
result = this.updateModuleAreaOrChildren(result, source, "areas", "query", []);
|
}
|
if (Array.isArray(result)) console.log("result is Array! Post areas");
|
if (source.children || source["-children"] || source["+children"] || source["<children"]) {
|
result = this.updateModuleAreaOrChildren(result, source, "children", "tag", ["index", "classes"]);
|
}
|
if (Array.isArray(result)) console.log("result is Array! Post children");
|
result = this.updateClasses(result, source);
|
if (Array.isArray(result)) console.log("result is Array! Post updateClasses");
|
result = this.updateCustomCSS(result, source);
|
if (Array.isArray(result)) console.log("result is Array! Post customCSS");
|
result = this.updateStyleVars(result, source);
|
if (Array.isArray(result)) console.log("result is Array! Post updateStyleVars");
|
result = this.updateProperties(result, source);
|
if (Array.isArray(result)) console.log("result is Array! Post updateProperties");
|
result = this.updateEvents(result, source);
|
if (Array.isArray(result)) console.log("result is Array! Post updateEvents");
|
result = this.updateValue(result, source, "default");
|
if (Array.isArray(result)) console.log("result is Array! Post updateValue default");
|
result = this.updateValue(result, source, "index");
|
if (Array.isArray(result)) console.log("result is Array! Post updateValue index");
|
result = this.updateValue(result, source, "queryAll");
|
if (Array.isArray(result)) console.log("result is Array! Post updateValue queryAll");
|
return result;
|
};
|
updateLocales = (target, source) => {
|
const removeAction = (target_, source_, key_) => {
|
const result = target_;
|
result[key_] = undefined;
|
return result;
|
};
|
const defaultAction = (target_, source_, key_) => {
|
const result = target_;
|
if (source_[key_] !== undefined) {
|
if (result[key_] === undefined) {
|
result[key_] = {};
|
}
|
const localeKeys = Object.keys(source_[key_]);
|
for (const localeKey of localeKeys) {
|
result[key_][localeKey] = source_[key_][localeKey];
|
}
|
}
|
return result;
|
};
|
const localeMerge = this.performUpdateAction(target, source, "locales", removeAction, defaultAction) || target;
|
const overrideMerge = this.performUpdateAction(localeMerge, source, "locale-overrides", removeAction, defaultAction) || localeMerge;
|
if (overrideMerge) {
|
return overrideMerge;
|
}
|
return target;
|
};
|
updateRoot = (target, source) => {
|
const merged = this.updateModuleOrArea(target, source);
|
return this.updateLocales(merged, source);
|
};
|
|
getFirstDefinedSource = (sources, sourceCount) => {
|
const result = { source: {}, index: -1 };
|
for (let index = 0; index < sourceCount; ++index) {
|
if (sources[index] != undefined) {
|
result.source = sources[index];
|
result.index = index;
|
break;
|
}
|
}
|
return result;
|
};
|
|
getTestCaseFiles(dir) {
|
const files = fs.readdirSync(dir);
|
return files
|
.filter(file => file.endsWith(".json") && file !== "result.json" && file !== "error.json")
|
.map(file => path.join(dir, file));
|
}
|
|
loadJsonFile(filePath) {
|
const data = fs.readFileSync(filePath, "utf-8");
|
return JSON.parse(data);
|
}
|
|
merge = (...sources) => {
|
this.errors = [];
|
|
if (!sources.length) return {};// no sources, return empty
|
const sourceCount = sources.length;
|
if (sourceCount < 1) return {};
|
|
const firstDefined = this.getFirstDefinedSource(sources, sourceCount);
|
if (firstDefined.index == -1) return {};// prevent merge if all sources are undefined
|
|
let merged = firstDefined.source;
|
for (let index = firstDefined.index + 1; index < sourceCount; ++index) {
|
const source = sources[index];
|
if (source == undefined) continue;// skip undefined sources
|
merged = this.updateRoot(merged, source);
|
}
|
merged.mergeErrors = this.errors;
|
return merged;
|
};
|
|
testMerge = () => {
|
const processDirectory = (currentDir) => {
|
const dirents = fs.readdirSync(currentDir, { withFileTypes: true });
|
|
dirents.forEach(dirent => {
|
try {
|
if (dirent.isDirectory()) {
|
const currentTestDir = path.join(currentDir, dirent.name);
|
const jsonFiles = this.getTestCaseFiles(currentTestDir);
|
const resultFilePath = path.join(currentTestDir, "result.json");
|
|
if (jsonFiles.length === 0) {
|
console.log(`No files found for test case ${dirent.name}!`);
|
return;
|
}
|
if (!fs.existsSync(resultFilePath)) {
|
console.log(`Result file found for test case ${dirent.name}!`);
|
return;
|
}
|
|
const sources = jsonFiles.map(file => this.loadJsonFile(file));
|
const result = this.merge(...sources);
|
const desiredResult = this.loadJsonFile(resultFilePath);
|
|
if (JSON.stringify(result) === JSON.stringify(desiredResult)) {
|
console.log(`Merge successful for ${dirent.name}!`);
|
}
|
else {
|
console.log(`Merge failed for ${dirent.name}!`);
|
fs.writeFileSync(path.join(currentTestDir, "error.json"), JSON.stringify(result, null, "\t").replace(/\n/g, os.EOL));
|
}
|
}
|
} catch (e) {
|
console.log(`Merge failed for ${dirent.name}: ${e}!`);
|
}
|
});
|
};
|
|
console.log("*** START LAYOUT JSON MERGE TESTS ***");
|
const testDirectory = "C:/furnview_dev/data/test/json";
|
processDirectory(testDirectory);
|
console.log("*** END LAYOUT JSON MERGE TESTS ***");
|
};
|
}
|
|
const LayoutConfigReaderService = new LayoutConfigReaderServiceClass();
|
|
module.exports = {
|
readMergedLayoutConfig: (configuration) => {
|
try {
|
return LayoutConfigReaderService.merge(configuration.modularLayoutRootConfigurations, configuration.modularLayoutLayoutConfigurations, configuration.modularLayout);
|
} catch (ex) {
|
return undefined;
|
}
|
},
|
testMerge: () => {
|
LayoutConfigReaderService.testMerge();
|
}
|
};
|