From 009e9cc7b6cf0a68c9db7aaaa6bcc6ab183512d4 Mon Sep 17 00:00:00 2001 From: David Okulski <32730627+DavidOkulski@users.noreply.github.com> Date: Wed, 23 Jul 2025 09:03:46 -0700 Subject: [PATCH 01/15] Increase pod count and add tini --- dockerfile | 5 ++++- helm/values.yaml | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/dockerfile b/dockerfile index e888a36..0b3878d 100644 --- a/dockerfile +++ b/dockerfile @@ -21,7 +21,8 @@ RUN apt-get update && apt-get install -y \ libxrandr2 \ libxrender1 \ xdg-utils \ - && rm -rf /var/lib/apt/lists/* + tini \ + && rm -rf /var/lib/apt/lists/* # Install Chromium RUN apt-get update && apt-get install -y chromium && rm -rf /var/lib/apt/lists/* @@ -35,6 +36,8 @@ RUN npm install COPY . . +ENTRYPOINT ["/usr/bin/tini", "--"] + EXPOSE 3030 CMD ["npm", "run", "start"] diff --git a/helm/values.yaml b/helm/values.yaml index 650c430..0311d31 100644 --- a/helm/values.yaml +++ b/helm/values.yaml @@ -2,7 +2,7 @@ replicaCount: 1 image: repository: ghcr.io/bcgov/communication-layer - tag: test + tag: latest pullPolicy: Always service: From c8d72969d1a2f0ccb7cf3ef379690aec0753dbd6 Mon Sep 17 00:00:00 2001 From: David Okulski <32730627+DavidOkulski@users.noreply.github.com> Date: Wed, 23 Jul 2025 09:07:05 -0700 Subject: [PATCH 02/15] Increase pod count --- helm/values.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/values.yaml b/helm/values.yaml index 0311d31..5acaabf 100644 --- a/helm/values.yaml +++ b/helm/values.yaml @@ -1,4 +1,4 @@ -replicaCount: 1 +replicaCount: 2 image: repository: ghcr.io/bcgov/communication-layer From 56872e083c9fccdb356c140fe24b192ffadfbd90 Mon Sep 17 00:00:00 2001 From: Viswam Date: Thu, 24 Jul 2025 15:12:06 -0700 Subject: [PATCH 03/15] Changes for Submit --- portal/generatePortalIntegratedHandler.js | 6 ++- portal/savePortalFormDataHandler.js | 65 +++++++++++++++++++++++ routes.js | 2 + 3 files changed, 72 insertions(+), 1 deletion(-) create mode 100644 portal/savePortalFormDataHandler.js diff --git a/portal/generatePortalIntegratedHandler.js b/portal/generatePortalIntegratedHandler.js index 55b5658..210be3f 100644 --- a/portal/generatePortalIntegratedHandler.js +++ b/portal/generatePortalIntegratedHandler.js @@ -24,7 +24,9 @@ async function generatePortalIntegratedTemplate(req, res) { if(!targetApp) { return res.status(400).send({ error: 'Unknown app ID' }); } - const paramsFromPortal = await getParametersFromPortal(targetApp,token,userId); + const paramsFromPortal = await getParametersFromPortal(targetApp,token,userId); + console.log("paramsFromPortal",paramsFromPortal); + if(!paramsFromPortal) { return res.status(400).send({ error: 'Parameters not found to generate form' }); @@ -71,4 +73,6 @@ async function constructFormJson(params) { return fullJSON; } + + module.exports = generatePortalIntegratedTemplate; \ No newline at end of file diff --git a/portal/savePortalFormDataHandler.js b/portal/savePortalFormDataHandler.js new file mode 100644 index 0000000..dee7b05 --- /dev/null +++ b/portal/savePortalFormDataHandler.js @@ -0,0 +1,65 @@ +async function submitForPortalAction (req,res) { + console.log("in here submitForPortalAction"); + + const { tokenId, savedForm ,config} = req.body; + console.log("tokenId",tokenId); + console.log("savedForm",savedForm); + console.log("config",config); +try{ + if (!config?.actions || !Array.isArray(config.actions)) { + return res + .status(400) + .send({ error: getErrorMessage("FORM_ID_REQUIRED") }); + } + for (const action of config.actions) { + if (action.actionType === 'endpoint') { + await handleEndpointAction(tokenId,action, savedForm); + } else if (action.actionType === 'email') { + await handleEmailAction(action, formData); + } else { + console.warn('Unexpected action type:', action.type); + return res + .status(400) + .send({ error: "Unexpected action type" }); + } + } + + res.json({ status: 'success' }); +} catch(error) { + return res + .status(400) + .send({ error: "Error in executing action type" }); +} +} +async function handleEndpointAction(tokenId,action, formData) { + console.log("action",action); + try { + const url = `${action.host}${action.path}`; + const headers = Object.fromEntries(action.headers.map(h => Object.entries(h)[0])); + + + const savedJson = { + "token": tokenId, + "formJson": formData + }; + const actionBody = Object.fromEntries( + (action.body || []).map(b => Object.entries(b)[0]) + ); + const payload = { ...savedJson, ...actionBody }; + const response = await fetch(url, { + method: action.type, + headers: { ...headers, Authorization: `Bearer ${action.authentication}`, 'Content-Type': 'application/json' }, + body: JSON.stringify(payload) + }); + + if (!response.ok) { + const text = await response.text(); + console.log("Error",text) + throw new Error(`Endpoint error: ${response.status} ${text}`); + } +} catch(error) { + console.error('Error in handleEndPoint:', error); + throw error; +} +} +module.exports = submitForPortalAction; \ No newline at end of file diff --git a/routes.js b/routes.js index a98c753..86bf927 100644 --- a/routes.js +++ b/routes.js @@ -9,6 +9,7 @@ const renderRouter = require("./renderHandler"); const {generatePDFFromHTML,generatePDFFromURL,generatePDFFromJSON,loadSavedJson } = require("./generatePDFHandler"); const generatePortalIntegratedTemplate = require("./portal/generatePortalIntegratedHandler.js"); +const submitForPortalAction = require("./portal/savePortalFormDataHandler.js"); const getFormsFromFormTemplate = require("./formRepoHandler"); @@ -145,5 +146,6 @@ router.post("/loadSavedJson", loadSavedJson); router.post("/generatePortalForm", generatePortalIntegratedTemplate); router.post("/generateNewTemplate", generateNewTemplate); router.use('/pdfRender', renderRouter); +router.use('/submitForPortalAction', submitForPortalAction); module.exports = router; \ No newline at end of file From 55dcf15ddbfa2eed8997958c0e52090eba3f33fd Mon Sep 17 00:00:00 2001 From: Viswam Date: Fri, 25 Jul 2025 15:51:25 -0700 Subject: [PATCH 04/15] Changes for Submit action 2799 --- errorHandling/errorMessages.json | 21 +++- portal/generatePortalIntegratedHandler.js | 24 ++--- portal/loadPortalDataHandler.js | 24 ++--- portal/savePortalFormDataHandler.js | 117 +++++++++++----------- 4 files changed, 99 insertions(+), 87 deletions(-) diff --git a/errorHandling/errorMessages.json b/errorHandling/errorMessages.json index 12207ff..6306e74 100644 --- a/errorHandling/errorMessages.json +++ b/errorHandling/errorMessages.json @@ -12,7 +12,12 @@ "FORM_NOT_FOUND_IN_REQUEST":"The form is not found in the incoming request. Try again later and report to support, if the issue is not solved", "GENERATE_ERROR_MSG":"Error generating the form .Close this window and try again in a few seconds", "FORM_ALREADY_FINALIZED":"The form you are trying to generate is already finalized", - "FORM_CANNOT_BE_GENERATED":"The form cannot be generated." + "FORM_CANNOT_BE_GENERATED":"The form cannot be generated.", + "PARAMS_NOT_FOUND":"Parameters not found to generate form", + "UNKNOWN_ORIGIN_SERVER":"Unknown Origin Server", + "NO_ACTION_FOUND":"No action found", + "UNKNOWN_ACTION":"Unknown action encountered", + "ERROR_IN_EXECUTING_ACTION":"Error in executing action type" }, "test": { "FORM_ID_REQUIRED": "Close this window and try again in a few seconds", @@ -27,7 +32,12 @@ "FORM_NOT_FOUND_IN_REQUEST":"Try again later and report to support, if the issue is not solved", "GENERATE_ERROR_MSG":"Close this window and try again in a few seconds", "FORM_ALREADY_FINALIZED":"The form you are trying to generate is already finalized", - "FORM_CANNOT_BE_GENERATED":"The form cannot be generated." + "FORM_CANNOT_BE_GENERATED":"The form cannot be generated.", + "PARAMS_NOT_FOUND":"Parameters not found to generate form", + "UNKNOWN_ORIGIN_SERVER":"Unknown Origin Server", + "NO_ACTION_FOUND":"No action found", + "UNKNOWN_ACTION":"Unknown action encountered", + "ERROR_IN_EXECUTING_ACTION":"Error in executing action type" }, "prod": { "FORM_ID_REQUIRED": "Close this window and try again in a few seconds", @@ -42,6 +52,11 @@ "FORM_NOT_FOUND_IN_REQUEST":"Try again later and report to support, if the issue is not solved", "GENERATE_ERROR_MSG":"Close this window and try again in a few seconds", "FORM_ALREADY_FINALIZED":"The form you are trying to generate is already finalized", - "FORM_CANNOT_BE_GENERATED":"The form cannot be generated." + "FORM_CANNOT_BE_GENERATED":"The form cannot be generated.", + "PARAMS_NOT_FOUND":"Parameters not found to generate form", + "UNKNOWN_ORIGIN_SERVER":"Unknown Origin Server", + "NO_ACTION_FOUND":"No action found", + "UNKNOWN_ACTION":"Unknown action encountered", + "ERROR_IN_EXECUTING_ACTION":"Error in executing action type" } } \ No newline at end of file diff --git a/portal/generatePortalIntegratedHandler.js b/portal/generatePortalIntegratedHandler.js index 1b4edd0..582b08d 100644 --- a/portal/generatePortalIntegratedHandler.js +++ b/portal/generatePortalIntegratedHandler.js @@ -8,36 +8,29 @@ const {getParametersFromPortal , expireTokenInPortal} = require("./loadPortalDa async function generatePortalIntegratedTemplate(req, res) { try { const params = req.body; - const token = params["id"]; + const token = params["id"]; + const userId = "test"; if (!token ) { return res .status(400) .send({ error: 'No token found' }); - } - const userId = "test"; - const portalId = params["portalId"]; - //const targetApp = appConfig[portalId]; + } const rawHost = (req.get("X-Original-Server") || req.hostname); const targetApp = appConfig[rawHost]; if(!targetApp) { - return res.status(400).send({ error: 'Unknown app ID' }); + return res.status(400).send({ error: getErrorMessage("UNKNOWN_ORIGIN_SERVER") }); } - const paramsFromPortal = await getParametersFromPortal(targetApp,token,userId); - console.log("paramsFromPortal",paramsFromPortal); - + const paramsFromPortal = await getParametersFromPortal(targetApp,token,userId); if(!paramsFromPortal) { - return res.status(400).send({ error: 'Parameters not found to generate form' }); + return res.status(400).send({ error: getErrorMessage("PARAMS_NOT_FOUND") }); } - const formJson = await constructFormJson(paramsFromPortal); if (formJson != null) { - //expire token - const isTokenExpired = await expireTokenInPortal(targetApp,token,userId); - console.log("isTokenExpired >> ",isTokenExpired); + res.status(200).send({ save_data: formJson }); @@ -48,9 +41,10 @@ async function generatePortalIntegratedTemplate(req, res) { } } catch (error) { console.error(`Error generating the form:`, error); + return res .status(400) - .send({ error: getErrorMessage("GENERATE_ERROR_MSG") }); + .send({ error: getErrorMessage("FORM_CANNOT_BE_GENERATED") }); } } diff --git a/portal/loadPortalDataHandler.js b/portal/loadPortalDataHandler.js index 799e83f..303309d 100644 --- a/portal/loadPortalDataHandler.js +++ b/portal/loadPortalDataHandler.js @@ -1,13 +1,11 @@ const axios = require("axios"); const { getErrorMessage } = require("../errorHandling/errorHandler.js"); -async function getParametersFromPortal(portal,token, userId) { - //call another api from portal to get the params - //TODO - add header for validation +async function getParametersFromPortal(portal,token, userId) { let parametersForForm = ""; try { - const urlForValidateTokenAndGetParams= portal.baseUrl+ (portal.getParametersEndpoint || process.env.PORTAL_VALIDATE_TOKEN_ENDPOINT); - console.log("urlForValidateTokenAndGetParams",urlForValidateTokenAndGetParams); + const urlForValidateTokenAndGetParams= portal.apiHost+ (portal.getParametersEndpoint || process.env.PORTAL_VALIDATE_TOKEN_ENDPOINT); + console.log("urlForValidateTokenAndGetParams >>",urlForValidateTokenAndGetParams); const response = await axios.post(`${urlForValidateTokenAndGetParams}`, { token, @@ -18,14 +16,16 @@ async function getParametersFromPortal(portal,token, userId) { 'Content-Type': 'application/json' } } - ); - - parametersForForm = response.data; + ); + return response.data; + + } catch (err) { - console.log( 'Failed to contact target app', err ); - return parametersForForm; + console.log( 'Failed to contact target app', err ); + + throw err; } - return parametersForForm; + } async function expireTokenInPortal(portal,token, userId) { @@ -34,7 +34,7 @@ async function expireTokenInPortal(portal,token, userId) { let isTokenExpired = false; try { - const urlForExpiringToken = portal.baseUrl + (portal.expireTokenEndPoint || process.env.PORTAL_EXPIRE_TOKEN_ENDPOINT);; + const urlForExpiringToken = portal.apiHost + (portal.expireTokenEndPoint || process.env.PORTAL_EXPIRE_TOKEN_ENDPOINT);; console.log("urlForExpiringToken",urlForExpiringToken); const response = await axios.post(`${urlForExpiringToken}`, { diff --git a/portal/savePortalFormDataHandler.js b/portal/savePortalFormDataHandler.js index dee7b05..57b1f20 100644 --- a/portal/savePortalFormDataHandler.js +++ b/portal/savePortalFormDataHandler.js @@ -1,65 +1,68 @@ +const appConfig = require('../appConfig.js'); async function submitForPortalAction (req,res) { - console.log("in here submitForPortalAction"); + const { tokenId, savedForm ,config} = req.body; + try{ + + if (!config?.actions || !Array.isArray(config.actions)) { + return res + .status(400) + .send({ error: getErrorMessage("NO_ACTION_FOUND")}); + } + const rawHost = (req.get("X-Original-Server") || req.hostname); + const portalConfig = appConfig[rawHost] || Object.values(appConfig).find(cfg => { + try { + return new URL(cfg.apiHost).hostname === rawHost; + } catch { + return false; + } + }) || {}; + + for (const action of config.actions) { + if (action.actionType === 'endpoint') { + await handleEndpointAction(tokenId,action, savedForm,portalConfig); + } else { + console.warn('Unexpected action type:', action.type); + return res + .status(400) + .send({ error: getErrorMessage("UNKNOWN_ACTION") }); + } + } - const { tokenId, savedForm ,config} = req.body; - console.log("tokenId",tokenId); - console.log("savedForm",savedForm); - console.log("config",config); -try{ - if (!config?.actions || !Array.isArray(config.actions)) { - return res - .status(400) - .send({ error: getErrorMessage("FORM_ID_REQUIRED") }); - } - for (const action of config.actions) { - if (action.actionType === 'endpoint') { - await handleEndpointAction(tokenId,action, savedForm); - } else if (action.actionType === 'email') { - await handleEmailAction(action, formData); - } else { - console.warn('Unexpected action type:', action.type); + res.json({ status: 'success' }); + } catch(error) { return res - .status(400) - .send({ error: "Unexpected action type" }); + .status(400) + .send({ error: getErrorMessage("ERROR_IN_EXECUTING_ACTION") }); } - } - - res.json({ status: 'success' }); -} catch(error) { - return res - .status(400) - .send({ error: "Error in executing action type" }); -} } -async function handleEndpointAction(tokenId,action, formData) { - console.log("action",action); - try { - const url = `${action.host}${action.path}`; - const headers = Object.fromEntries(action.headers.map(h => Object.entries(h)[0])); - +async function handleEndpointAction(tokenId,action, formData, portalConfig) { + + try { + const portalHost = portalConfig["apiHost"] || action.host; + const url = portalHost+action.path; + const headers = Object.fromEntries(action.headers.map(h => Object.entries(h)[0])); - const savedJson = { - "token": tokenId, - "formJson": formData - }; - const actionBody = Object.fromEntries( - (action.body || []).map(b => Object.entries(b)[0]) - ); - const payload = { ...savedJson, ...actionBody }; - const response = await fetch(url, { - method: action.type, - headers: { ...headers, Authorization: `Bearer ${action.authentication}`, 'Content-Type': 'application/json' }, - body: JSON.stringify(payload) - }); - - if (!response.ok) { - const text = await response.text(); - console.log("Error",text) - throw new Error(`Endpoint error: ${response.status} ${text}`); - } -} catch(error) { - console.error('Error in handleEndPoint:', error); - throw error; -} + const savedJson = { + "token": tokenId, + "formJson": formData + }; + const actionBody = Object.fromEntries( + (action.body || []).map(b => Object.entries(b)[0]) + ); + const payload = { ...savedJson, ...actionBody }; + const response = await fetch(url, { + method: action.type, + headers: { ...headers, Authorization: `Bearer ${action.authentication}`, 'Content-Type': 'application/json' }, + body: JSON.stringify(payload) + }); + + if (!response.ok) { + const text = await response.text(); + throw new Error(`Endpoint error: ${response.status} ${text}`); + } + } catch(error) { + console.error('Error in handleEndPoint:', error); + throw error; + } } module.exports = submitForPortalAction; \ No newline at end of file From 40729714ce100bea154a2572e1c5a94f2dd583af Mon Sep 17 00:00:00 2001 From: David Okulski <32730627+DavidOkulski@users.noreply.github.com> Date: Mon, 28 Jul 2025 11:26:41 -0700 Subject: [PATCH 05/15] added support for databinding conditions --- databindingsHandler.js | 135 +++++++++++++++++++++++++---------------- 1 file changed, 84 insertions(+), 51 deletions(-) diff --git a/databindingsHandler.js b/databindingsHandler.js index c2f5d26..d36f3ba 100644 --- a/databindingsHandler.js +++ b/databindingsHandler.js @@ -21,7 +21,7 @@ async function populateDatabindings(formJson, params) { } // Map the API data to the form fields - const boundData = bindDataToFields(formJson, apiData); + const boundData = bindDataToFields(formJson, apiData, params); return boundData; } catch (error) { @@ -51,7 +51,50 @@ async function fetchDataFromSources(dataSources, params) { return data; } -function bindDataToFields(formJson, fetchedData) { +function getBindingValue(bindings, fetchedData, params = {}) { + // Normalize singular binding to array if needed + let list; + if (Array.isArray(bindings)) { + list = bindings; + } else if (bindings && bindings.source && bindings.path) { + list = [{ + order: 0, + condition: '', + source: bindings.source, + path: bindings.path + }]; + } else { + return null; + } + + const sortedBindings = list.slice().sort((a, b) => (a.order || 0) - (b.order || 0)); + + // Evaluate each binding + for (const binding of sortedBindings) { + // Parse condition + const [conditionField, valuesListString = ''] = (binding.condition || '').split('=').map(str => str.trim()); + + // Evaluate condition + if (conditionField) { + const allowedValues = valuesListString.split(',').map(str => str.trim()); + const currentParam = params[conditionField]; + if (!allowedValues.includes(currentParam)) { + continue; + } + } + + const sourceData = fetchedData[binding.source] || {}; + const results = JSONPath({ path: binding.path, json: sourceData }); + + if (Array.isArray(results) && results.length > 0 && results[0] != null && results[0] !== '') { + return results[0]; + } + } + + return null; +} + +function bindDataToFields(formJson, fetchedData, params = {}) { const formData = {}; const processItemsForDatabinding = (items) => { @@ -59,72 +102,62 @@ function bindDataToFields(formJson, fetchedData) { if (field.type === 'container' && field.containerItems) { processItemsForDatabinding(field.containerItems); - } else if (field.databindings != null) { - - const dataSourceName = field.databindings.source; - const dataPath = field.databindings.path; - const fetchedSourceData = fetchedData[dataSourceName]; - let valueFromPath = ""; - - // Fetch the value from the fetched data - if (fetchedSourceData) { - valueFromPath = JSONPath(dataPath, fetchedSourceData); - // If the field is a group, handle groupItems - if (field.type === 'group' && field.groupItems) { - // Create an array to store groupData objects for each item in valueFromPath - const groupDataArray = valueFromPath.map((pathObj, index) => { - const groupData = {}; - - // For each item in valueFromPath, iterate over the group fields - field.groupItems.forEach(groupItem => { - groupItem.fields.forEach(groupField => { - if (groupField.databindings) { - const fieldBindings = groupField.databindings.path; - const fieldIdInGroup = `${field.id}-${index}-${groupField.id}`; - // Assign data from pathObj or an empty string if not available - groupData[fieldIdInGroup] = transformValueToBindIfNeeded(groupField, pathObj[fieldBindings]) || ''; - } + + } else if (field.databindings) { + // Use new array‑based bindings if present, otherwise wrap the legacy binding + const bindings = Array.isArray(field.databindings) + ? field.databindings + : [ field.databindings ]; + const raw = getBindingValue(bindings, fetchedData, params); + + if (field.type === 'group' && field.groupItems) { + const groupDataArray = raw && Array.isArray(raw) + ? raw.map((pathObj, index) => { + const groupData = {}; + field.groupItems.forEach(groupItem => { + groupItem.fields.forEach(groupField => { + if (groupField.databindings) { + const gf = Array.isArray(groupField.databindings) ? groupField.databindings[0] : groupField.databindings; + const fieldIdInGroup = `${field.id}-${index}-${groupField.id}`; + const subVal = JSONPath({ path: gf.path, json: pathObj })[0]; + groupData[fieldIdInGroup] = transformValueToBindIfNeeded(groupField, subVal) || ''; + } + }); }); - }); - return groupData; - }); - - // Assign the groupDataArray to formData for this field's ID - formData[field.id] = groupDataArray; - } else { - formData[field.id] = valueFromPath.length > 0 ? transformValueToBindIfNeeded(field, valueFromPath[0]) : null; - } + return groupData; + }) + : []; + formData[field.id] = groupDataArray; + + } else { + formData[field.id] = raw != null + ? transformValueToBindIfNeeded(field, raw) + : null; } - } else if (field.type === 'group' && field.groupItems && !field.repeater) { + } + + else if (field.type === 'group' && field.groupItems && !field.repeater) { const transformedItem = {}; - //get the databindings from individual filed from non-repeating group field.groupItems.forEach(groupItem => { groupItem.fields.forEach(groupField => { if (groupField.databindings) { - const dataSourceName = groupField.databindings.source; - const dataPath = groupField.databindings.path; - const fetchedSourceData = fetchedData[dataSourceName]; - const fieldIdInGroup = `${field.id}-0-${groupField.id}`; - if (fetchedSourceData) { - const valueFromPathForGroupField = JSONPath(dataPath, fetchedSourceData); - //do date conversion here uisng a function - chceking the type as date and then checking format and applying - transformedItem[fieldIdInGroup] = valueFromPathForGroupField.length > 0 ? transformValueToBindIfNeeded(groupField, valueFromPathForGroupField[0]) : null; // Replace with actual value + const gf = Array.isArray(groupField.databindings) ? groupField.databindings[0] : groupField.databindings; + const fetchedSourceData = fetchedData[gf.source] || {}; + const vals = JSONPath({ path: gf.path, json: fetchedSourceData }); + const id = `${field.id}-0-${groupField.id}`; + transformedItem[id] = vals.length > 0 ? transformValueToBindIfNeeded(groupField, vals[0]): null; } - } }); }); formData[field.id] = [transformedItem]; } }); - } + }; if (formJson?.data?.items) { processItemsForDatabinding(formJson.data.items); } - // Iterate through fields - formJson.data.items.forEach - return formData; } From 5cd10beced9d18c83613aae584c188432488e1c9 Mon Sep 17 00:00:00 2001 From: David Okulski <32730627+DavidOkulski@users.noreply.github.com> Date: Mon, 28 Jul 2025 11:41:04 -0700 Subject: [PATCH 06/15] Update formatting --- databindingsHandler.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/databindingsHandler.js b/databindingsHandler.js index d36f3ba..b413813 100644 --- a/databindingsHandler.js +++ b/databindingsHandler.js @@ -104,10 +104,8 @@ function bindDataToFields(formJson, fetchedData, params = {}) { processItemsForDatabinding(field.containerItems); } else if (field.databindings) { - // Use new array‑based bindings if present, otherwise wrap the legacy binding - const bindings = Array.isArray(field.databindings) - ? field.databindings - : [ field.databindings ]; + // Use new array‑based bindings, otherwise wrap the legacy binding + const bindings = Array.isArray(field.databindings) ? field.databindings : [ field.databindings ]; const raw = getBindingValue(bindings, fetchedData, params); if (field.type === 'group' && field.groupItems) { From 027288b7dbaf119d784100363d1011754c8b3509 Mon Sep 17 00:00:00 2001 From: Danilo Meireles Date: Thu, 31 Jul 2025 15:25:08 -0700 Subject: [PATCH 07/15] Update gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 68f5a24..4d85769 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /node_modules .env /common/* +.idea/ \ No newline at end of file From 2e526164cfda2af7ccd43c190dcf837c562da07d Mon Sep 17 00:00:00 2001 From: Danilo Meireles Date: Thu, 31 Jul 2025 15:33:10 -0700 Subject: [PATCH 08/15] Update error messages. --- errorHandling/errorMessages.json | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/errorHandling/errorMessages.json b/errorHandling/errorMessages.json index 12207ff..474abd8 100644 --- a/errorHandling/errorMessages.json +++ b/errorHandling/errorMessages.json @@ -12,7 +12,9 @@ "FORM_NOT_FOUND_IN_REQUEST":"The form is not found in the incoming request. Try again later and report to support, if the issue is not solved", "GENERATE_ERROR_MSG":"Error generating the form .Close this window and try again in a few seconds", "FORM_ALREADY_FINALIZED":"The form you are trying to generate is already finalized", - "FORM_CANNOT_BE_GENERATED":"The form cannot be generated." + "FORM_CANNOT_BE_GENERATED":"The form cannot be generated.", + "INVALID_FORM_ID":"Invalid Form ID.", + "INVALID_TOOL":"Invalid Tool." }, "test": { "FORM_ID_REQUIRED": "Close this window and try again in a few seconds", @@ -27,7 +29,9 @@ "FORM_NOT_FOUND_IN_REQUEST":"Try again later and report to support, if the issue is not solved", "GENERATE_ERROR_MSG":"Close this window and try again in a few seconds", "FORM_ALREADY_FINALIZED":"The form you are trying to generate is already finalized", - "FORM_CANNOT_BE_GENERATED":"The form cannot be generated." + "FORM_CANNOT_BE_GENERATED":"The form cannot be generated.", + "INVALID_FORM_ID":"Invalid Form ID.", + "INVALID_TOOL":"Invalid Tool." }, "prod": { "FORM_ID_REQUIRED": "Close this window and try again in a few seconds", @@ -42,6 +46,8 @@ "FORM_NOT_FOUND_IN_REQUEST":"Try again later and report to support, if the issue is not solved", "GENERATE_ERROR_MSG":"Close this window and try again in a few seconds", "FORM_ALREADY_FINALIZED":"The form you are trying to generate is already finalized", - "FORM_CANNOT_BE_GENERATED":"The form cannot be generated." + "FORM_CANNOT_BE_GENERATED":"The form cannot be generated.", + "INVALID_FORM_ID":"Invalid Form ID.", + "INVALID_TOOL":"Invalid Tool." } } \ No newline at end of file From 2d4b00b37b146d9817b516c00cca80ccdf684711 Mon Sep 17 00:00:00 2001 From: Danilo Meireles Date: Thu, 31 Jul 2025 15:36:26 -0700 Subject: [PATCH 09/15] Add validation --- generateHandler.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/generateHandler.js b/generateHandler.js index 9d4fd3c..8d6044b 100644 --- a/generateHandler.js +++ b/generateHandler.js @@ -141,6 +141,18 @@ async function generateNewTemplate(req, res) { .send({ error: getErrorMessage("FORM_ALREADY_FINALIZED") }); } + if (icm_metadata["Template"] !== template_id) { + return res + .status(400) + .send({ error: getErrorMessage("INVALID_FORM_ID") }); + } + + if (icm_metadata["Tool"]?.toLowerCase() !== "formfoundry") { + return res + .status(400) + .send({ error: getErrorMessage("INVALID_TOOL") }); + } + const formJson = await constructFormJson(template_id, params); if (formJson != null) { From 2519e17c84739e4fb0c8b09afdb032a44858b17c Mon Sep 17 00:00:00 2001 From: David Okulski <32730627+DavidOkulski@users.noreply.github.com> Date: Fri, 1 Aug 2025 08:47:39 -0700 Subject: [PATCH 10/15] refactor databindings --- databindingsHandler.js | 87 ++++++++++++++++++++---------------------- 1 file changed, 41 insertions(+), 46 deletions(-) diff --git a/databindingsHandler.js b/databindingsHandler.js index b413813..8861a7a 100644 --- a/databindingsHandler.js +++ b/databindingsHandler.js @@ -97,65 +97,60 @@ function getBindingValue(bindings, fetchedData, params = {}) { function bindDataToFields(formJson, fetchedData, params = {}) { const formData = {}; - const processItemsForDatabinding = (items) => { - items.forEach(field => { + //helper function to bind group + function bindRowsToGroup(field, rows) { + return rows.map((rowObj, index) => { + const row = {}; + const binding = Array.isArray(field.databindings) ? field.databindings[0] : field.databindings || null; + const localDatabindings = binding ? { ...fetchedData, [binding.source]: rowObj } : { ...fetchedData }; + + field.groupItems.forEach(groupItem => + groupItem.fields.forEach(groupField => { + if (!groupField.databindings) return; + const fieldIdInGroup = `${field.id}-${index}-${groupField.id}`; + const subVal = getBindingValue(groupField.databindings,localDatabindings,params); + row[fieldIdInGroup] = transformValueToBindIfNeeded(groupField, subVal) || ''; + }) + ); + return row; + }); + } + function processItemsForDatabinding(items) { + items.forEach(field => { + // containers if (field.type === 'container' && field.containerItems) { - processItemsForDatabinding(field.containerItems); - - } else if (field.databindings) { - // Use new array‑based bindings, otherwise wrap the legacy binding - const bindings = Array.isArray(field.databindings) ? field.databindings : [ field.databindings ]; - const raw = getBindingValue(bindings, fetchedData, params); - - if (field.type === 'group' && field.groupItems) { - const groupDataArray = raw && Array.isArray(raw) - ? raw.map((pathObj, index) => { - const groupData = {}; - field.groupItems.forEach(groupItem => { - groupItem.fields.forEach(groupField => { - if (groupField.databindings) { - const gf = Array.isArray(groupField.databindings) ? groupField.databindings[0] : groupField.databindings; - const fieldIdInGroup = `${field.id}-${index}-${groupField.id}`; - const subVal = JSONPath({ path: gf.path, json: pathObj })[0]; - groupData[fieldIdInGroup] = transformValueToBindIfNeeded(groupField, subVal) || ''; - } - }); - }); - return groupData; - }) - : []; - formData[field.id] = groupDataArray; + return processItemsForDatabinding(field.containerItems); + } + // groups + if (field.type === 'group' && field.groupItems) { + if (field.repeater) { + // repeatable group + const binding = Array.isArray(field.databindings) ? field.databindings[0] : field.databindings || null; + const rows = binding ? JSONPath({ path: binding.path, json: fetchedData }) || [] : []; + formData[field.id] = bindRowsToGroup(field, rows); } else { - formData[field.id] = raw != null - ? transformValueToBindIfNeeded(field, raw) - : null; + // single-instance group + const binding = Array.isArray(field.databindings) ? field.databindings[0]: field.databindings || null; + const rows = binding? (JSONPath({ path: binding.path, json: fetchedData }) || []).slice(0, 1) : [{}]; + formData[field.id] = bindRowsToGroup(field, rows); } + return; } - else if (field.type === 'group' && field.groupItems && !field.repeater) { - const transformedItem = {}; - field.groupItems.forEach(groupItem => { - groupItem.fields.forEach(groupField => { - if (groupField.databindings) { - const gf = Array.isArray(groupField.databindings) ? groupField.databindings[0] : groupField.databindings; - const fetchedSourceData = fetchedData[gf.source] || {}; - const vals = JSONPath({ path: gf.path, json: fetchedSourceData }); - const id = `${field.id}-0-${groupField.id}`; - transformedItem[id] = vals.length > 0 ? transformValueToBindIfNeeded(groupField, vals[0]): null; - } - }); - }); - formData[field.id] = [transformedItem]; + //Single fields + if (field.databindings) { + const raw = getBindingValue(field.databindings, fetchedData, params); + formData[field.id] = raw != null ? transformValueToBindIfNeeded(field, raw) : null; } }); }; - if (formJson?.data?.items) { + if (Array.isArray(formJson?.data?.items)) { processItemsForDatabinding(formJson.data.items); } - + // console.dir(formData, { depth: null, colors: true }); return formData; } From 0f585eaa5d442bc5eb2f121ac19a6b144c7a4ed7 Mon Sep 17 00:00:00 2001 From: Danilo Meireles Date: Fri, 1 Aug 2025 11:51:23 -0700 Subject: [PATCH 11/15] Pass Template and Tool properties. --- saveICMdataHandler.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/saveICMdataHandler.js b/saveICMdataHandler.js index 748e04e..0f0efeb 100644 --- a/saveICMdataHandler.js +++ b/saveICMdataHandler.js @@ -19,6 +19,9 @@ async function getICMAttachmentStatus(attachment_id, username, params) { return_data["Locked by Id"] = ""; return_data["DocFileName"] = ""; return_data["Office Name"] = ""; + return_data["Template"] = ""; + return_data["Tool"] = ""; + if (!attachment_id || attachment_id == "") { return return_data; } @@ -45,6 +48,8 @@ async function getICMAttachmentStatus(attachment_id, username, params) { return_data["Locked by Id"] = response.data["Locked by Id"]; return_data["DocFileName"] = response.data["DocFileName"]; return_data["Office Name"] = response.data["Office Name"]; + return_data["Template"] = response.data["Template"]; + return_data["Tool"] = response.data["Tool"]; return return_data; } catch (error) { From 22605eba3a4049fcebb29a5420af12075beae08c Mon Sep 17 00:00:00 2001 From: David Okulski <32730627+DavidOkulski@users.noreply.github.com> Date: Fri, 1 Aug 2025 12:47:41 -0700 Subject: [PATCH 12/15] Updated schema to support new Klamm schema --- schema/form_definition.yaml | 70 +++++++++++++++++++++++-------------- schema/saved_json.yaml | 3 +- 2 files changed, 46 insertions(+), 27 deletions(-) diff --git a/schema/form_definition.yaml b/schema/form_definition.yaml index 392badb..a7f4345 100644 --- a/schema/form_definition.yaml +++ b/schema/form_definition.yaml @@ -7,11 +7,11 @@ required: properties: version: anyOf: - - type: string - - type: number + - type: string + - type: number ministry_id: anyOf: - - type: string + - type: string - type: number id: type: string @@ -25,13 +25,13 @@ properties: type: string deployed_to: anyOf: - - type: string + - type: string - type: "null" dataSources: type: array items: type: object - default: [] + default: [] data: type: object required: @@ -64,8 +64,8 @@ definitions: type: string mask: anyOf: - - type: string - - type: "null" + - type: string + - type: "null" codeContext: type: object properties: @@ -90,7 +90,7 @@ definitions: type: string required: - text - - value + - value groupItems: type: array items: @@ -101,7 +101,7 @@ definitions: items: $ref: "#/definitions/Item" repeater: - type: boolean + type: boolean helperText: anyOf: - type: string @@ -142,7 +142,7 @@ definitions: value: anyOf: - type: string - - type: number + - type: number - type: boolean errorMessage: anyOf: @@ -151,7 +151,7 @@ definitions: required: - type - value - - errorMessage + - errorMessage conditions: type: array items: @@ -169,30 +169,48 @@ definitions: required: - type - value - additionalProperties: false + additionalProperties: false webStyles: anyOf: - type: "null" - type: object - additionalProperties: - type: string + additionalProperties: + anyOf: + - type: string + - type: number + - type: boolean + - type: object + - type: array + - type: "null" pdfStyles: anyOf: - type: "null" - type: object - additionalProperties: - type: string + additionalProperties: + anyOf: + - type: string + - type: number + - type: boolean + - type: object + - type: array + - type: "null" databindings: - type: object - properties: - path: - type: string - source: - type: string - required: - - path - - source - additionalProperties: false + type: array + items: + type: object + properties: + path: + type: string + source: + type: string + order: + type: number + condition: + type: string + required: + - path + - source + additionalProperties: false containerItems: type: array items: diff --git a/schema/saved_json.yaml b/schema/saved_json.yaml index 7712db5..6011102 100644 --- a/schema/saved_json.yaml +++ b/schema/saved_json.yaml @@ -1,3 +1,4 @@ + type: object required: - data @@ -13,7 +14,6 @@ properties: - type: number - type: object - type: array - - type: "null" items: type: object additionalProperties: @@ -23,6 +23,7 @@ properties: - type: number - type: object - type: "null" + - type: "null" form_definition: $ref: "form_definition_schema" metadata: From c13f52a8d80e83d38e29024f36c2ad530603bdd0 Mon Sep 17 00:00:00 2001 From: Danilo Meireles Date: Fri, 1 Aug 2025 14:32:27 -0700 Subject: [PATCH 13/15] Add area validation. --- errorHandling/errorMessages.json | 9 ++++++--- generateHandler.js | 8 ++++++++ saveICMdataHandler.js | 2 ++ 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/errorHandling/errorMessages.json b/errorHandling/errorMessages.json index 474abd8..21045cd 100644 --- a/errorHandling/errorMessages.json +++ b/errorHandling/errorMessages.json @@ -14,7 +14,8 @@ "FORM_ALREADY_FINALIZED":"The form you are trying to generate is already finalized", "FORM_CANNOT_BE_GENERATED":"The form cannot be generated.", "INVALID_FORM_ID":"Invalid Form ID.", - "INVALID_TOOL":"Invalid Tool." + "INVALID_TOOL":"Invalid Tool.", + "INVALID_AREA":"Invalid Area." }, "test": { "FORM_ID_REQUIRED": "Close this window and try again in a few seconds", @@ -31,7 +32,8 @@ "FORM_ALREADY_FINALIZED":"The form you are trying to generate is already finalized", "FORM_CANNOT_BE_GENERATED":"The form cannot be generated.", "INVALID_FORM_ID":"Invalid Form ID.", - "INVALID_TOOL":"Invalid Tool." + "INVALID_TOOL":"Invalid Tool.", + "INVALID_AREA":"Invalid Area." }, "prod": { "FORM_ID_REQUIRED": "Close this window and try again in a few seconds", @@ -48,6 +50,7 @@ "FORM_ALREADY_FINALIZED":"The form you are trying to generate is already finalized", "FORM_CANNOT_BE_GENERATED":"The form cannot be generated.", "INVALID_FORM_ID":"Invalid Form ID.", - "INVALID_TOOL":"Invalid Tool." + "INVALID_TOOL":"Invalid Tool.", + "INVALID_AREA":"Invalid Area." } } \ No newline at end of file diff --git a/generateHandler.js b/generateHandler.js index 8d6044b..01918ea 100644 --- a/generateHandler.js +++ b/generateHandler.js @@ -153,6 +153,14 @@ async function generateNewTemplate(req, res) { .send({ error: getErrorMessage("INVALID_TOOL") }); } + let requestArea = params["area"]; + let icmArea = icm_metadata["Categorie"]; + if (requestArea && requestArea.toLowerCase() !== icmArea?.toLowerCase()) { + return res + .status(400) + .send({ error: getErrorMessage("INVALID_AREA") }); + } + const formJson = await constructFormJson(template_id, params); if (formJson != null) { diff --git a/saveICMdataHandler.js b/saveICMdataHandler.js index 0f0efeb..5c508ac 100644 --- a/saveICMdataHandler.js +++ b/saveICMdataHandler.js @@ -21,6 +21,7 @@ async function getICMAttachmentStatus(attachment_id, username, params) { return_data["Office Name"] = ""; return_data["Template"] = ""; return_data["Tool"] = ""; + return_data["Categorie"] = ""; if (!attachment_id || attachment_id == "") { return return_data; @@ -50,6 +51,7 @@ async function getICMAttachmentStatus(attachment_id, username, params) { return_data["Office Name"] = response.data["Office Name"]; return_data["Template"] = response.data["Template"]; return_data["Tool"] = response.data["Tool"]; + return_data["Categorie"] = response.data["Categorie"]; return return_data; } catch (error) { From 68e09aa3749c014a85216b49f1e4b4fccd9f8458 Mon Sep 17 00:00:00 2001 From: David Okulski <32730627+DavidOkulski@users.noreply.github.com> Date: Tue, 5 Aug 2025 11:34:34 -0700 Subject: [PATCH 14/15] Update form definition to support old databindings --- schema/form_definition.yaml | 51 +++++++++++++++++++++++++------------ 1 file changed, 35 insertions(+), 16 deletions(-) diff --git a/schema/form_definition.yaml b/schema/form_definition.yaml index a7f4345..d988ba6 100644 --- a/schema/form_definition.yaml +++ b/schema/form_definition.yaml @@ -195,22 +195,41 @@ definitions: - type: array - type: "null" databindings: - type: array - items: - type: object - properties: - path: - type: string - source: - type: string - order: - type: number - condition: - type: string - required: - - path - - source - additionalProperties: false + oneOf: + - type: array + items: + type: object + properties: + path: + type: string + source: + type: string + order: + type: number + condition: + anyOf: + - type: string + - type: "null" + required: + - path + - source + additionalProperties: false + - type: object + properties: + path: + type: string + source: + type: string + order: + type: number + condition: + anyOf: + - type: string + - type: "null" + required: + - path + - source + additionalProperties: false containerItems: type: array items: From 5073992171fc12e7c8b852c22e7970f8228817df Mon Sep 17 00:00:00 2001 From: Danilo Meireles Date: Tue, 5 Aug 2025 16:15:21 -0700 Subject: [PATCH 15/15] Fix the area validation logic. --- generateHandler.js | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/generateHandler.js b/generateHandler.js index 01918ea..f764690 100644 --- a/generateHandler.js +++ b/generateHandler.js @@ -153,12 +153,20 @@ async function generateNewTemplate(req, res) { .send({ error: getErrorMessage("INVALID_TOOL") }); } - let requestArea = params["area"]; - let icmArea = icm_metadata["Categorie"]; - if (requestArea && requestArea.toLowerCase() !== icmArea?.toLowerCase()) { - return res - .status(400) - .send({ error: getErrorMessage("INVALID_AREA") }); + const requestArea = params.area?.toLowerCase(); + + if (requestArea) { + const icmArea = icm_metadata.Categorie?.toLowerCase(); + + const isServiceRequest = requestArea === "service request"; + + const isAreaValid = + (isServiceRequest && icmArea === "application") || + (!isServiceRequest && requestArea === icmArea); + + if (!isAreaValid) { + return res.status(400).send({ error: getErrorMessage("INVALID_AREA") }); + } } const formJson = await constructFormJson(template_id, params);