Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
314ce43
Add new endpoint to process json clob data
danilomeireles Aug 8, 2025
03963e0
Process clob data data.
danilomeireles Aug 9, 2025
d96e3d6
Update router.js
danilomeireles Aug 10, 2025
5afb659
Refactor processIcmJsonClobData
danilomeireles Aug 10, 2025
21bf42a
Code cleanup.
danilomeireles Aug 10, 2025
ccd46ea
Use existing settings for siebel url and username
danilomeireles Aug 11, 2025
accf5f8
Fix URL path
danilomeireles Aug 12, 2025
b91ad53
(ADO-3158) truncate json UUID before XML creation, remove index/offes…
NicolaSDPR1 Aug 12, 2025
77221e2
Merge pull request #81 from bcgov/ADO-3158-JSON-XML-Changes
bzimonjaSDPR Aug 14, 2025
41355cd
Add new endpoint to process json clob data
danilomeireles Aug 8, 2025
c7859fd
Process clob data data.
danilomeireles Aug 9, 2025
7007a82
Update router.js
danilomeireles Aug 10, 2025
5c9e449
Refactor processIcmJsonClobData
danilomeireles Aug 10, 2025
d5d8345
Code cleanup.
danilomeireles Aug 10, 2025
c7cd6ea
Use existing settings for siebel url and username
danilomeireles Aug 11, 2025
82dfe40
Fix URL path
danilomeireles Aug 12, 2025
c6ccad0
Update databindings host/endpoint processing
DavidOkulski Aug 15, 2025
9f7eb30
Merge branch 'ADO-2495' of https://github.com/bcgov/Communication-Lay…
danilomeireles Aug 15, 2025
6fac249
Update the attachement id value.
danilomeireles Aug 15, 2025
f09d46a
Create a second endpoint with only the answers in the response
danilomeireles Aug 18, 2025
37fdee5
Rename answers endpoint
danilomeireles Aug 18, 2025
308dd81
Update response object.
danilomeireles Aug 18, 2025
7039f9a
Merge pull request #79 from bcgov/ADO-2495
bzimonjaSDPR Aug 18, 2025
c373f46
Quick fix using XMLDEC to ensure the beginning xml tag does not inclu…
NicolaSDPR1 Aug 19, 2025
44af38c
Merge pull request #82 from bcgov/ADO-3158-XML-quick-fix
bzimonjaSDPR Aug 19, 2025
dfa66b7
Remove answer drilldown
DavidOkulski Aug 20, 2025
c156809
Use apiHost for portal endpoints
DavidOkulski Aug 20, 2025
cd130c8
Merge pull request #83 from bcgov/pr-portalendpoint-apiHost
DavidOkulski Aug 20, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 38 additions & 6 deletions databindingsHandler.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ async function fetchDataFromSources(dataSources, params) {

let updatedParams = updateParams(source.params || {}, params, data);
const response = await readJsonFormApi(source, { ...params, ...updatedParams });
console.log("Response:",response);
data[source.name] = response;

} catch (error) {
Expand Down Expand Up @@ -187,6 +188,7 @@ async function readJsonFormApi(datasource, pathParams) {
const headers = {
Authorization: `Bearer ${grant.id_token.token}`,
"X-ICM-TrustedUsername": username,
"X-API-Host": apiHost,
}
if (type.toUpperCase() === 'GET') {
// For GET requests, add params directly in axios config
Expand All @@ -199,6 +201,7 @@ async function readJsonFormApi(datasource, pathParams) {
}

// Store response data
console.log("Response:",response);
return ensureObjectOrArray(response.data);

} catch (error) {
Expand All @@ -210,6 +213,10 @@ async function readJsonFormApi(datasource, pathParams) {
function buildUrlWithParams(host, endpoint, pathVariables) {
const hostFromEnv = getHost(pathVariables,host);
const endpointFromEnv = getEndpoint(endpoint);

if (!hostFromEnv) throw new Error("API host not resolved");
if (!endpointFromEnv) throw new Error("API endpoint not resolved");

let url = `${hostFromEnv}${endpointFromEnv}`;
// Replace any placeholder variables like @@attachmentId
Object.keys(pathVariables).forEach(key => {
Expand All @@ -222,18 +229,23 @@ function buildUrlWithParams(host, endpoint, pathVariables) {


function getHost(params, host) {
// Use host from environment variable if available, otherwise fall back to JSON
try {
return params["apiHost"];
} catch {
return process.env[host];
// Use process.env if present
const fromEnv = resolveMaybeEnv(host);
if (fromEnv) return fromEnv;

//Fall back to path params
if (params && typeof params.apiHost === "string" && params.apiHost.trim()) {
return params.apiHost.trim();
}

return host ? (process.env[host] ?? host) : null;
}

function getEndpoint(endpoint) {
// Use endpoint from environment variable if available, otherwise fall back to JSON
return process.env[endpoint] || endpoint;
return resolveMaybeEnv(endpoint) ?? endpoint;
}

function buildBodyWithParams(bodyFromJson, pathVariables) {

let bodyString = JSON.stringify(bodyFromJson);
Expand Down Expand Up @@ -316,5 +328,25 @@ function updateParams(params, pathParams = {}, allFetchedData = {}) {
return updated;
}

function resolveMaybeEnv(str) {
if (typeof str !== "string" || !str) {
return null;
}

// Check if value is in the form "process.env.VAR_NAME"
const envPattern = /^process\.env\.(.+)$/;
const match = str.match(envPattern);

if (!match) {
return str;
}

const envVarName = match[1];
const envValue = process.env[envVarName];

return envValue != null ? envValue : str;
}


module.exports.populateDatabindings = populateDatabindings;
module.exports.buildUrlWithParams = buildUrlWithParams;
99 changes: 99 additions & 0 deletions icmJsonClobHandler.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
const axios = require("axios");
const { keycloakForSiebel } = require("./keycloak.js");

function parseIcmJsonClob(item) {
if (item.ICMJsonClob && typeof item.ICMJsonClob === 'string') {
try {
item.ICMJsonClobParsed = JSON.parse(item.ICMJsonClob);
delete item.ICMJsonClob;
} catch (error) {
console.warn(`Failed to parse ICMJsonClob for item ${item.Id || 'unknown'}: ${error.message}`);
item.ICMJsonClobParsed = null;
}
}
return item;
}

function processIcmJsonClobData(jsonData) {

// Create a copy to avoid mutating the original instance
const processedData = JSON.parse(JSON.stringify(jsonData));

if (processedData.items && Array.isArray(processedData.items)) {
processedData.items = processedData.items.map(parseIcmJsonClob);
return processedData;
}

return parseIcmJsonClob(processedData);
}

async function fetchIcmJsonClobData(attachmentId, apiHost) {
try {
const grant = await keycloakForSiebel.grantManager.obtainFromClientCredentials();

const username = process.env.TRUSTED_USERNAME;

const headers = {
Authorization: `Bearer ${grant.id_token.token}`,
"X-ICM-TrustedUsername": username,
"Content-Type": "application/json"
};

const url = `${apiHost}${process.env.ICM_JSON_CLOB_ENDPONT}`;

const queryParams = {
ViewMode: "Catalog",
SearchSpec: `UserFieldCLOB=\"${attachmentId}\"`
};

return await axios.get(url, {
headers,
params: queryParams,
timeout: 30000
});

} catch (error) {
throw new Error(`Failed to fetch ICM data for SR ID ${attachmentId}: ${error.message}`);
}
}

function extractICMJsonClobParsed(data) {
if (data.items && Array.isArray(data.items)) {
return data.items
.map(item => item.ICMJsonClobParsed)
.filter(parsed => parsed !== null && parsed !== undefined);
}

if (data.ICMJsonClobParsed !== null && data.ICMJsonClobParsed !== undefined) {
return data.ICMJsonClobParsed;
}

return [];
}

async function getProcessedData(attachmentId, apiHost, returnAnswersOnly = true) {
try {
const response = await fetchIcmJsonClobData(attachmentId, apiHost);
let processedResult = processIcmJsonClobData(response.data);

if (returnAnswersOnly) {
processedResult = extractICMJsonClobParsed(processedResult);
}

return {
success: true,
data: processedResult
};
} catch (error) {
console.error("API call failed:", error.message);

return {
success: false,
error: error.message
};
}
}

module.exports = {
getProcessedData
};
63 changes: 57 additions & 6 deletions routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,39 @@ const { generateTemplate,generateNewTemplate } = require("./generateHandler");
const { saveICMdata, loadICMdata, clearICMLockedFlag } = require("./saveICMdataHandler");
const { getUsername } = require("./usernameHandler.js");
const renderRouter = require("./renderHandler");
const appCfg = require('./appConfig.js');

const {generatePDFFromHTML,generatePDFFromURL,generatePDFFromJSON,loadSavedJson } = require("./generatePDFHandler");
const generatePortalIntegratedTemplate = require("./portal/generatePortalIntegratedHandler.js");
const submitForPortalAction = require("./portal/savePortalFormDataHandler.js");
const loadPortalIntegratedForm = require("./portal/loadPortalIntegratedHandler.js");


const getFormsFromFormTemplate = require("./formRepoHandler");
require("./formRepoHandler");
const {getProcessedData} = require("./icmJsonClobHandler");
const router = express.Router();

const FORM_SERVER_URL = process.env.FORMSERVERURL;
const ENDPOINT_URL = process.env.ENDPOINTURL;

const localhostOnlyMiddleware = (req, res, next) => {
const clientIP = req.ip ||
req.connection.remoteAddress ||
req.socket.remoteAddress ||
(req.connection.socket ? req.connection.socket.remoteAddress : null);

const actualIP = clientIP?.replace('::ffff:', '') || clientIP;

const isLocalhost = clientIP === '127.0.0.1' ||
clientIP === '::1' ||
clientIP === 'localhost' ||
actualIP?.startsWith('172.') || // Docker default bridge network
actualIP?.startsWith('192.168.') || // Docker custom networks
actualIP?.startsWith('10.'); // Docker swarm networks

if (!isLocalhost) {
return res.status(403).json({ error: 'Localhost access only' });
}
next();
};

// Form Map
const formMap = new Map();
Expand Down Expand Up @@ -115,8 +135,7 @@ router.post("/generate", generateTemplate);

router.get("/getAllForms", async (request, response) => {
try {
const grant =
await keycloakForFormRepo.grantManager.obtainFromClientCredentials();
const grant = await keycloakForFormRepo.grantManager.obtainFromClientCredentials();
let endpointUrl = `http://localhost:3030/api/forms-list`;

const forms = await axios.get(endpointUrl, {
Expand All @@ -134,10 +153,41 @@ router.get("/getAllForms", async (request, response) => {

});

router.get("/processIcmJsonClob", localhostOnlyMiddleware, async (req, res) => {
try {
const apiHost = req.get("X-API-Host");
const result = await getProcessedData(req.query.attachmentId, apiHost, false);

if (result.success) {
res.status(200).json(result.data);
} else {
res.status(500).json({ error: "Error." });
}
} catch (error) {
console.error("Error:", error.message);
res.status(500).json({ error: "Internal server error", message: error.message });
}
});

router.get("/processIcmJsonClobAnswers", localhostOnlyMiddleware, async (req, res) => {
try {
const apiHost = req.get("X-API-Host");
const result = await getProcessedData(req.query.attachmentId, apiHost, true);

if (result.success) {
res.status(200).json(result.data);
} else {
res.status(500).json({ error: "Error." });
}
} catch (error) {
console.error("Error:", error.message);
res.status(500).json({ error: "Internal server error", message: error.message });
}
});

// clear the locked by flags in ICM for the form, used when form is closed
router.post("/clearICMLockedFlag", clearICMLockedFlag);


router.post("/generatePDFFromJson", generatePDFFromJSON);

// Generate route
Expand All @@ -149,4 +199,5 @@ router.post("/generateNewTemplate", generateNewTemplate);
router.use('/pdfRender', renderRouter);
router.use('/submitForPortalAction', submitForPortalAction);
router.post("/loadPortalForm", loadPortalIntegratedForm);

module.exports = router;
26 changes: 24 additions & 2 deletions saveICMdataHandler.js
Original file line number Diff line number Diff line change
Expand Up @@ -133,8 +133,30 @@ async function saveICMdata(req, res) {
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
let builder = new xml2js.Builder();
saveJson["XML Hierarchy"] = builder.buildObject(saveData);
const truncatedKeysSaveData = {};
for(let oldKey in saveData) { //This begins trunicating the JSON keys for XML (UUID should be first 8 characters)
const stringLength = oldKey.length;
const newKey = oldKey.substring(0, stringLength-28);
if (Array.isArray(saveData[oldKey]) > 0 && Object.keys(saveData[oldKey]).length > 0) { //This trunicates child/dependant objects
const childrenArray = [];
for(let i = 0; i < saveData[oldKey].length; i++) {
const truncatedChildrenKeys = {};
for (let oldChildKey in saveData[oldKey][i]) {
const childStringLength = oldChildKey.length;
const newChildKey = oldChildKey.substring(stringLength+3, childStringLength-28);
truncatedChildrenKeys[newChildKey] = saveData[oldKey][i][oldChildKey];
}
childrenArray.push(truncatedChildrenKeys);
}
const wrapperKey = {}
wrapperKey[newKey] = childrenArray;
truncatedKeysSaveData[`${newKey}-List`] = wrapperKey // Add a wrapper around the children/dependecies
} else {
truncatedKeysSaveData[newKey] = saveData[oldKey]; //Data is added to new JSON with the truncated key
}
}
let builder = new xml2js.Builder({xmldec: { version: '1.0' }});
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 + '/', '');
let url = buildUrlWithParams(params["apiHost"], params["saveEndpoint"] + attachment_id + '/', params);
try {
Expand Down