diff --git a/package-lock.json b/package-lock.json index 7a0bc34..c447cfd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "axios": "^1.7.4", "cors": "^2.8.5", "date-fns": "^4.1.0", + "diff": "^8.0.3", "dotenv": "^16.4.5", "express": "^4.19.2", "express-http-proxy": "^2.0.0", @@ -73,6 +74,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.9.tgz", "integrity": "sha512-lWBYIrF7qK5+GjY5Uy+/hEgp8OJWOD/rpy74GplYRhEauvbHDeFB8t5hPOZxCZ0Oxf4Cc36tK51/l3ymJysrKw==", "dev": true, + "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.26.2", @@ -972,6 +974,7 @@ "version": "1.6.0", "resolved": "https://registry.npmjs.org/@redis/client/-/client-1.6.0.tgz", "integrity": "sha512-aR0uffYI700OEEH4gYnitAnv3vzVGXCFvYfdpu/CJKvk4pHfLPEy/JSZyrpQ+15WhXe1yJRXLtfQ84s4mEXnPg==", + "peer": true, "dependencies": { "cluster-key-slot": "1.1.2", "generic-pool": "3.9.0", @@ -1630,6 +1633,7 @@ "url": "https://github.com/sponsors/ai" } ], + "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001688", "electron-to-chromium": "^1.5.73", @@ -2195,7 +2199,17 @@ "node_modules/devtools-protocol": { "version": "0.0.1380148", "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1380148.tgz", - "integrity": "sha512-1CJABgqLxbYxVI+uJY/UDUHJtJ0KZTSjNYJYKqd9FRoXT33WDakDHNxRapMEgzeJ/C3rcs01+avshMnPmKQbvA==" + "integrity": "sha512-1CJABgqLxbYxVI+uJY/UDUHJtJ0KZTSjNYJYKqd9FRoXT33WDakDHNxRapMEgzeJ/C3rcs01+avshMnPmKQbvA==", + "peer": true + }, + "node_modules/diff": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.3.tgz", + "integrity": "sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } }, "node_modules/diff-sequences": { "version": "29.6.3", @@ -3964,6 +3978,7 @@ "version": "1.3.9", "resolved": "https://registry.npmjs.org/jsep/-/jsep-1.3.9.tgz", "integrity": "sha512-i1rBX5N7VPl0eYb6+mHNp52sEuaS2Wi8CDYx1X5sn9naevL78+265XJqy1qENEk7mRKwS06NHpUqiBwR7qeodw==", + "peer": true, "engines": { "node": ">= 10.16.0" } diff --git a/package.json b/package.json index b6abf80..d27d7b4 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "axios": "^1.7.4", "cors": "^2.8.5", "date-fns": "^4.1.0", + "diff": "^8.0.3", "dotenv": "^16.4.5", "express": "^4.19.2", "express-http-proxy": "^2.0.0", diff --git a/routes.js b/routes.js index 6fcb57e..c68ca34 100644 --- a/routes.js +++ b/routes.js @@ -3,7 +3,7 @@ const axios = require("axios"); const xmlparser = require("express-xml-bodyparser"); const { keycloakForSiebel, keycloakForFormRepo } = require("./keycloak.js"); const { generateTemplate,generateNewTemplate } = require("./generateHandler"); -const { saveICMdata, loadICMdata, clearICMLockedFlag, loadICMdataAsPDF } = require("./saveICMdataHandler"); +const { saveICMdata, loadICMdata, clearICMLockedFlag, loadICMdataAsPDF, compareSaveICMdata } = require("./saveICMdataHandler"); const { getUsername } = require("./usernameHandler.js"); const renderRouter = require("./renderHandler"); const appCfg = require('./appConfig.js'); @@ -129,7 +129,7 @@ router.post("/saveData", async (request, response) => { // ICM save data route router.post("/saveICMData", saveICMdata); - +router.post("/compareICMData", compareSaveICMdata); // ICM load data rout router.post("/loadICMData", loadICMdata); diff --git a/saveICMdataHandler.js b/saveICMdataHandler.js index b3ff6ff..083ee09 100644 --- a/saveICMdataHandler.js +++ b/saveICMdataHandler.js @@ -12,6 +12,7 @@ const { formExceptions } = require("./dictionary/jsonXmlConversion.js"); const { propertyExists, propertyNotEmpty, keyExists } = require("./dictionary/dictionaryUtils.js"); const {generatePDF }= require("./generatePDFHandler.js"); const { param } = require("./renderHandler.js"); +const { createTwoFilesPatch } = require('diff'); const SIEBEL_ICM_API_FORMS_ENDPOINT = process.env.SIEBEL_ICM_API_FORMS_ENDPOINT; @@ -72,6 +73,33 @@ async function getICMAttachmentStatus(attachment_id, username, params, authHeade } } +async function compareSaveICMdata(req, res) { + // const attachment_id = req.body["attachmentId"]; + const savedFormJson = req.body["savedFormJson"]; + const savedFormXml = req.body["savedFormXml"]; + if (!savedFormJson) { + return res + .status(400) + .send({ error: getErrorMessage("FORM_NOT_FOUND_IN_REQUEST") }); + } + // console.log(savedFormParam) + const xml = buildICMXML(savedFormJson); + // console.log(xml); + + const patch = createTwoFilesPatch( + 'comm layer xml', + 'kiln api xml', + xml, + savedFormXml, + '', // old header + '', // new header + { context: 3 } + ); + console.log(patch); + + return res.status(200).send({patch}); +} + //method to save the form (data, template and metadata) as a JSON file in ICM, and update form // instance metadata with In Progress status, filename and extracted form data as an XML hierarchy async function saveICMdata(req, res) { @@ -87,14 +115,15 @@ async function saveICMdata(req, res) { }) || {}; params = { ...params,...configOpt }; const attachment_id = params["attachmentId"]; - const savedFormParam = params["savedForm"]; + const savedFormJson = params["savedFormJson"]; + const savedFormXML = params["savedFormXml"]; if (!attachment_id) { return res .status(400) .send({ error: getErrorMessage("ATTACHMENT_ID_REQUIRED") }); } - if (!savedFormParam) { + if (!savedFormJson | !savedFormXML) { return res .status(400) .send({ error: getErrorMessage("FORM_NOT_FOUND_IN_REQUEST") }); @@ -137,7 +166,7 @@ async function saveICMdata(req, res) { } //saveForm validate before saving to ICM and determine kiln version being used - const includesData = Object.keys(JSON.parse(savedFormParam)).includes("data"); // Check if data exists. + const includesData = Object.keys(JSON.parse(savedFormJson)).includes("data"); // Check if data exists. if (!includesData) { console.log('JSON is not valid '); return res @@ -145,7 +174,7 @@ async function saveICMdata(req, res) { .send({ error: getErrorMessage("FORM_NOT_VALID") }); } - const valid = isJsonStringValid(savedFormParam); + const valid = isJsonStringValid(savedFormJson); if (!valid) { console.log('JSON is not valid '); return res @@ -159,79 +188,14 @@ async function saveICMdata(req, res) { saveJson["Status"] = "In Progress"; saveJson["DocFileName"] = (form_metadata["DocFileName"] && form_metadata["DocFileName"] !== "") ? form_metadata["DocFileName"] : attachment_id.replace(/^[^-]+-/, 'Form_'); saveJson["DocFileExt"] = "json"; - saveJson["Doc Attachment Id"] = Buffer.from(savedFormParam).toString('base64');//savedForm is saved as attachment - let saveData = JSON.parse(savedFormParam)["data"];// This is the data part of the savedJson - - /** - * Apply Kiln Version - * Kiln V1 uses data: { items: []} - * Kiln V2 uses dataSources [] - */ - const kilnVersion = Object.keys(JSON.parse(savedFormParam)["form_definition"]["data"]).includes("items") ? 1 : 2; - const formDefinitionItems = kilnVersion === 1 ? JSON.parse(savedFormParam)["form_definition"]["data"]["items"] : JSON.parse(savedFormParam)["form_definition"]["elements"];// This is the field info for form items - - // dateItemsId : This will contain all of the IDs of the date fields - // checkboxItemsId : This will contain all of the IDs of the checkbox fields - const { dateItemsId, checkboxItemsId, textInfoFields } = getFormIds(formDefinitionItems); - - const dictionary = formExceptions; - const formId = JSON.parse(savedFormParam)["form_definition"]["form_id"]; // Get the form ID - const formVersion = JSON.parse(savedFormParam)["form_definition"]["version"]; // Get the form version - const isFormException = keyExists(dictionary, formId); // If true, then this form will have its exceptions formatted - let toWrapIds = {}; //List of ids that will need to be placed in a wrapper. This only happens if form exception is true and wrapperTags exists - const noCheckboxChange = (isFormException && propertyExists(dictionary, formId, "allowCheckboxWithNoChange")) ? dictionary[formId].allowCheckboxWithNoChange : []; - const omitFields = (isFormException && propertyExists(dictionary, formId, "omitFields")) ? dictionary[formId].omitFields : []; - const addFields = (isFormException && propertyExists(dictionary, formId, "addFields")) ? dictionary[formId].addFields : {}; + saveJson["Doc Attachment Id"] = Buffer.from(savedFormJson).toString('base64');//savedForm is saved as attachment + saveJson["XML Hierarchy"] = savedFormXML; - if (isFormException && propertyExists(dictionary, formId, "wrapperTags")) { - dictionary[formId]["wrapperTags"].forEach((wrapperTag, index) => { - const tagKey = Object.keys(dictionary[formId]["wrapperTags"][index])[0]; - if (wrapperTag[tagKey].length != 0) { // If there are any wrappers with no fields, ignore. Otherwise, keep a list of UUID and the wrapper to put it in. - toWrapIds = {...toWrapIds, ...getWrapperIds(wrapperTag[tagKey], [tagKey])}; - } - }); - } - - // The updated JSON values required for XML creation - const truncatedKeysSaveData = fixJSONValuesForXML(saveData, {}, toWrapIds, dateItemsId, checkboxItemsId, textInfoFields, noCheckboxChange, omitFields, addFields, kilnVersion); - - let builder; // This will be for building the XML - if (isFormException) { // If any forms with the correct version (TODO) have been listed as exceptions, then proceed with their form exceptions - // If the root needs a differernt name, apply it here. Otherwise use the default "root" - if (propertyExists(dictionary, formId, "rootName") && propertyNotEmpty(dictionary, formId, "rootName")) { - builder = new xml2js.Builder({xmldec: { version: '1.0' }, renderOpts: { pretty: false }, rootName: dictionary[formId]["rootName"]}); - } else { - builder = new xml2js.Builder({xmldec: { version: '1.0' }, renderOpts: { pretty: false }}); - } - - let wrapperJson = truncatedKeysSaveData; - // If subRoots exist, wrap the sub-roots around the JSON where the last array object will be closest to JSON and first array object will be closest to root/rootName - if (propertyExists(dictionary, formId, "subRoots") && propertyNotEmpty(dictionary, formId, "subRoots")) { - wrapperJson = {}; - let tempJson = {}; - const subRootLength = dictionary[formId]["subRoots"].length; - for (i = subRootLength; i > 0; i= i -1) { - if (i === subRootLength) { - tempJson[dictionary[formId]["subRoots"][i-1]] = truncatedKeysSaveData; - } else { - wrapperJson[dictionary[formId]["subRoots"][i-1]] = tempJson; - tempJson = wrapperJson; - wrapperJson = {}; - } - } - wrapperJson = tempJson; - } - saveJson["XML Hierarchy"] = builder.buildObject(wrapperJson); - } else { - builder = new xml2js.Builder({xmldec: { version: '1.0' }, renderOpts: { pretty: false }}); - saveJson["XML Hierarchy"] = builder.buildObject(truncatedKeysSaveData); - } //let url = buildUrlWithParams('SIEBEL_ICM_API_HOST', 'fwd/v1.0/data/DT Form Instance Thin/DT Form Instance Thin/' + attachment_id + '/', ''); - const xml = saveJson["XML Hierarchy"]; - const xmlSize = Buffer.byteLength(xml, 'utf8'); // size in bytes + const xmlSize = Buffer.byteLength(savedFormXML, 'utf8'); // size in bytes // console.log("XML Hierarchy:", xml); - console.log("XML Hierarchy length (chars):", xml.length); + console.log("XML Hierarchy length (chars):", savedFormXML.length); console.log("XML Hierarchy size (bytes):", xmlSize); let url = buildUrlWithParams(params["apiHost"], params["saveEndpoint"] + attachment_id + '/', params); try { @@ -431,6 +395,75 @@ async function clearICMLockedFlag(req, res) { } +function buildICMXML(savedFormParam) { + /** + * Apply Kiln Version + * Kiln V1 uses data: { items: []} + * Kiln V2 uses dataSources [] + */ + const kilnVersion = Object.keys(JSON.parse(savedFormParam)["form_definition"]["data"]).includes("items") ? 1 : 2; + const formDefinitionItems = kilnVersion === 1 ? JSON.parse(savedFormParam)["form_definition"]["data"]["items"] : JSON.parse(savedFormParam)["form_definition"]["elements"];// This is the field info for form items + + // dateItemsId : This will contain all of the IDs of the date fields + // checkboxItemsId : This will contain all of the IDs of the checkbox fields + const { dateItemsId, checkboxItemsId, textInfoFields } = getFormIds(formDefinitionItems); + + const dictionary = formExceptions; + const formId = JSON.parse(savedFormParam)["form_definition"]["form_id"]; // Get the form ID + const formVersion = JSON.parse(savedFormParam)["form_definition"]["version"]; // Get the form version + const isFormException = keyExists(dictionary, formId); // If true, then this form will have its exceptions formatted + let toWrapIds = {}; //List of ids that will need to be placed in a wrapper. This only happens if form exception is true and wrapperTags exists + const noCheckboxChange = (isFormException && propertyExists(dictionary, formId, "allowCheckboxWithNoChange")) ? dictionary[formId].allowCheckboxWithNoChange : []; + const omitFields = (isFormException && propertyExists(dictionary, formId, "omitFields")) ? dictionary[formId].omitFields : []; + const addFields = (isFormException && propertyExists(dictionary, formId, "addFields")) ? dictionary[formId].addFields : {}; + + if (isFormException && propertyExists(dictionary, formId, "wrapperTags")) { + dictionary[formId]["wrapperTags"].forEach((wrapperTag, index) => { + const tagKey = Object.keys(dictionary[formId]["wrapperTags"][index])[0]; + if (wrapperTag[tagKey].length != 0) { // If there are any wrappers with no fields, ignore. Otherwise, keep a list of UUID and the wrapper to put it in. + toWrapIds = {...toWrapIds, ...getWrapperIds(wrapperTag[tagKey], [tagKey])}; + } + }); + } + + console.log(toWrapIds); + + // The updated JSON values required for XML creation + const truncatedKeysSaveData = fixJSONValuesForXML(JSON.parse(savedFormParam)["data"], {}, toWrapIds, dateItemsId, checkboxItemsId, textInfoFields, noCheckboxChange, omitFields, addFields, kilnVersion); + + let builder; // This will be for building the XML + if (isFormException) { // If any forms with the correct version (TODO) have been listed as exceptions, then proceed with their form exceptions + // If the root needs a differernt name, apply it here. Otherwise use the default "root" + if (propertyExists(dictionary, formId, "rootName") && propertyNotEmpty(dictionary, formId, "rootName")) { + builder = new xml2js.Builder({xmldec: { version: '1.0' }, renderOpts: { pretty: true }, rootName: dictionary[formId]["rootName"]}); + } else { + builder = new xml2js.Builder({xmldec: { version: '1.0' }, renderOpts: { pretty: true }}); + } + + let wrapperJson = truncatedKeysSaveData; + // If subRoots exist, wrap the sub-roots around the JSON where the last array object will be closest to JSON and first array object will be closest to root/rootName + if (propertyExists(dictionary, formId, "subRoots") && propertyNotEmpty(dictionary, formId, "subRoots")) { + wrapperJson = {}; + let tempJson = {}; + const subRootLength = dictionary[formId]["subRoots"].length; + for (i = subRootLength; i > 0; i= i -1) { + if (i === subRootLength) { + tempJson[dictionary[formId]["subRoots"][i-1]] = truncatedKeysSaveData; + } else { + wrapperJson[dictionary[formId]["subRoots"][i-1]] = tempJson; + tempJson = wrapperJson; + wrapperJson = {}; + } + } + wrapperJson = tempJson; + } + return builder.buildObject(wrapperJson); + } else { + builder = new xml2js.Builder({xmldec: { version: '1.0' }, renderOpts: { pretty: true }}); + return builder.buildObject(truncatedKeysSaveData); + } +} + /** Get the UUIDs (data.items "id"s) from the form for specific field types * @params formDefinitionItems = JSON.parse(savedFormParam)["form_definition"]["data"]["items"] * @returns dateItemsId : This will contain all of the IDs of the date fields @@ -444,9 +477,9 @@ function getFormIds (formDefinitionItems) { formDefinitionItems.forEach(item => { // Add the field types found in this loop into their specific item id arrays if (item.containerItems) { // Check for fields in containers (currently, there can be up to 5 container levels) item.containerItems.forEach(subItem => { - if (subItem.type === "date") dateItemsId.push(subItem.id); - else if (subItem.type === "checkbox") checkboxItemsId.push(subItem.id); - else if (subItem.type === "text-info") textInfoFields.push(subItem.id); + if (subItem.type === "date") dateItemsId.push(subItem.id || subItem.uuid); + else if (subItem.type === "checkbox-input") checkboxItemsId.push(subItem.id || subItem.uuid); + else if (subItem.type === "text-info") textInfoFields.push(subItem.id || subItem.uuid); else if (subItem.type === "container") { const {dateItemsId: recursiveDateItemIds, checkboxItemsId: recursiveCheckboxItemIds, textInfoFields: recursiveTextInfoFields} = getFormIds([subItem]); dateItemsId = [...dateItemsId, ...recursiveDateItemIds]; @@ -457,15 +490,15 @@ function getFormIds (formDefinitionItems) { } else if (item.groupItems){ // Check for fields in groups item.groupItems.forEach(subItem => { subItem.fields.forEach(childItemData => { // Group's fields is where the fields will be in - if (childItemData.type === "date") dateItemsId.push(childItemData.id); - else if (childItemData.type === "checkbox") checkboxItemsId.push(childItemData.id); - else if (childItemData.type === "text-info") textInfoFields.push(childItemData.id); + if (childItemData.type === "date") dateItemsId.push(childItemData.id || childItemData.uuid); + else if (childItemData.type === "checkbox-input") checkboxItemsId.push(childItemData.id || childItemData.uuid); + else if (childItemData.type === "text-info") textInfoFields.push(childItemData.id || childItemData.uuid); }); }); } else { - if (item.type === "date") dateItemsId.push(item.id); - else if (item.type === "checkbox") checkboxItemsId.push(item.id); - else if (item.type === "text-info") textInfoFields.push(item.id); + if (item.type === "date") dateItemsId.push(item.id || item.uuid); + else if (item.type === "checkbox-input") checkboxItemsId.push(item.id || item.uuid); + else if (item.type === "text-info") textInfoFields.push(item.id || item.uuid); } }); return { dateItemsId, checkboxItemsId, textInfoFields }; @@ -953,6 +986,7 @@ async function loadICMdataAsPDF(req,res) { } } +module.exports.compareSaveICMdata = compareSaveICMdata; module.exports.saveICMdata = saveICMdata; module.exports.loadICMdata = loadICMdata; module.exports.clearICMLockedFlag = clearICMLockedFlag;