diff --git a/.env.example b/.env.example index 02117e9..6a56aff 100644 --- a/.env.example +++ b/.env.example @@ -37,6 +37,12 @@ PORTAL_EXPIRE_TOKEN_ENDPOINT=expireToken APP_CONFIG='{ "localhost": { "apiHost":"someHost", + "apiKey": "secret", + "portalAuth": "basic", + "basicAuth": { + "username": "test", + "password": "thing" + }, "saveEndpoint":"someEndpoint", "employeeEndpoint":"someEndpoint" } diff --git a/.github/workflows/build-and-deploy.yml b/.github/workflows/build-and-deploy.yml index 122a17d..2c76cc4 100644 --- a/.github/workflows/build-and-deploy.yml +++ b/.github/workflows/build-and-deploy.yml @@ -1,76 +1,127 @@ -name: Build and Deploy - -on: - push: - branches: - - main - - dev - - test - -env: - REGISTRY: ghcr.io - IMAGE_NAME: ${{ github.repository }} - -jobs: - - build_and_push: - runs-on: ubuntu-latest - - steps: - - name: Checkout the repository - uses: actions/checkout@v4 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Log in to GitHub Container Registry - uses: docker/login-action@v3 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Extract metadata for Docker - id: meta - uses: docker/metadata-action@v5 - with: - images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} - tags: | - type=ref,event=branch,ref=${{ github.ref_name }} - - - name: Build and push Docker image - uses: docker/build-push-action@v5 - with: - context: . - push: true - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - - outputs: - image_tag: ${{ steps.meta.outputs.tags }} - - deploy: - needs: build_and_push - runs-on: ubuntu-latest - - steps: - - name: Checkout the repository - uses: actions/checkout@v4 - - - name: Install oc CLI - uses: redhat-actions/oc-installer@v1 - - - name: Authenticate with OpenShift - uses: redhat-actions/oc-login@v1 - with: - openshift_server_url: ${{ secrets.OPENSHIFT_SERVER }} - namespace: ${{ github.ref == 'refs/heads/main' && secrets.OPENSHIFT_PROD_NAMESPACE || (github.ref == 'refs/heads/dev' && secrets.OPENSHIFT_DEV_NAMESPACE) || secrets.OPENSHIFT_TEST_NAMESPACE }} - openshift_token: ${{ github.ref == 'refs/heads/main' && secrets.OPENSHIFT_PROD_TOKEN || (github.ref == 'refs/heads/dev' && secrets.OPENSHIFT_DEV_TOKEN) || secrets.OPENSHIFT_TEST_TOKEN }} - insecure_skip_tls_verify: true - - - name: Deploy with Helm - run: | - helm upgrade --install communication-layer ./helm --set image.repository=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} --set image.tag=${{ needs.build_and_push.outputs.image_tag }} - - name: Trigger OpenShift Rollout - run: | - oc rollout restart deployment/communication-layer +name: Build and Deploy + +on: + push: + branches: + - main + - dev + - test + workflow_dispatch: + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + +jobs: + + build_and_push: + runs-on: ubuntu-latest + + steps: + - name: Checkout the repository + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata for Docker + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=ref,event=branch,ref=${{ github.ref_name }} + + - name: Build and push Docker image + uses: docker/build-push-action@v5 + with: + context: . + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + + outputs: + image_tag: ${{ steps.meta.outputs.tags }} + + deploy: + needs: build_and_push + runs-on: ubuntu-latest + + steps: + - name: Checkout the repository + uses: actions/checkout@v4 + + - name: Install oc CLI + uses: redhat-actions/oc-installer@v1 + + - name: Authenticate with OpenShift + uses: redhat-actions/oc-login@v1 + with: + openshift_server_url: ${{ secrets.OPENSHIFT_SERVER }} + namespace: ${{ github.ref == 'refs/heads/main' && secrets.OPENSHIFT_PROD_NAMESPACE || (github.ref == 'refs/heads/dev' && secrets.OPENSHIFT_DEV_NAMESPACE) || secrets.OPENSHIFT_TEST_NAMESPACE }} + openshift_token: ${{ github.ref == 'refs/heads/main' && secrets.OPENSHIFT_PROD_TOKEN || (github.ref == 'refs/heads/dev' && secrets.OPENSHIFT_DEV_TOKEN) || secrets.OPENSHIFT_TEST_TOKEN }} + insecure_skip_tls_verify: true + + - name: Deploy with Helm + run: | + helm upgrade --install communication-layer ./helm --set image.repository=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} --set image.tag=${{ needs.build_and_push.outputs.image_tag }} + - name: Trigger OpenShift Rollout + run: | + oc rollout restart deployment/communication-layer + + # Deploy to Tools only if branch = dev + - name: Authenticate with OpenShift (tools env) + if: github.ref == 'refs/heads/dev' + uses: redhat-actions/oc-login@v1 + with: + openshift_server_url: ${{ secrets.OPENSHIFT_SERVER }} + namespace: ${{ secrets.OPENSHIFT_TOOLS_NAMESPACE }} + openshift_token: ${{ secrets.OPENSHIFT_TOOLS_TOKEN }} + insecure_skip_tls_verify: true + + - name: Deploy with Helm (tools env) + if: github.ref == 'refs/heads/dev' + run: | + helm upgrade --install communication-layer ./helm \ + --namespace "${{ secrets.OPENSHIFT_TOOLS_NAMESPACE }}" \ + --set image.repository=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} \ + --set image.tag=${{ needs.build_and_push.outputs.image_tag }} + + - name: Trigger OpenShift Rollout (tools env) + if: github.ref == 'refs/heads/dev' + run: | + oc rollout restart deployment/communication-layer \ + -n "${{ secrets.OPENSHIFT_TOOLS_NAMESPACE }}" + + deploy_to_portal: + needs: build_and_push + runs-on: ubuntu-latest + + steps: + - name: Checkout the repository + uses: actions/checkout@v4 + + - name: Install oc CLI + uses: redhat-actions/oc-installer@v1 + + - name: Authenticate with OpenShift + uses: redhat-actions/oc-login@v1 + with: + openshift_server_url: ${{ secrets.OPENSHIFT_SERVER }} + namespace: ${{ github.ref == 'refs/heads/main' && secrets.OPENSHIFT_PROD_PORTAL_NAMESPACE || (github.ref == 'refs/heads/dev' && secrets.OPENSHIFT_DEV_PORTAL_NAMESPACE) || secrets.OPENSHIFT_TEST_PORTAL_NAMESPACE }} + openshift_token: ${{ github.ref == 'refs/heads/main' && secrets.OPENSHIFT_PROD_PORTAL_TOKEN || (github.ref == 'refs/heads/dev' && secrets.OPENSHIFT_DEV_PORTAL_TOKEN) || secrets.OPENSHIFT_TEST_PORTAL_TOKEN }} + insecure_skip_tls_verify: true + + - name: Deploy with Helm + run: | + helm upgrade --install communication-layer ./helm --set image.repository=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} --set image.tag=${{ needs.build_and_push.outputs.image_tag }} + - name: Trigger OpenShift Rollout + run: | + oc rollout restart deployment/communication-layer diff --git a/app.js b/app.js index a63265e..920a9ec 100644 --- a/app.js +++ b/app.js @@ -1,41 +1,42 @@ -const express = require("express"); -const cors = require("cors"); -const env = require("dotenv").config(); -const routes = require("./routes"); -const swaggerUi = require("swagger-ui-express"); -const swaggerDocument = require("./swagger.json"); -const bodyParser = require("body-parser"); - -const app = express(); -const PORT = process.env.PORT || 3000; - -app.use(express.json({ limit: '50mb' })); -app.use(bodyParser.json()); - -app.use(express.urlencoded({ limit: '50mb', extended: true })); - - -// Serve Swagger UI at `/api-docs` -app.use("/api-docs", swaggerUi.serve, swaggerUi.setup(swaggerDocument)); - -const allowedOrigins = process.env.ALLOWEDORIGINS ? process.env.ALLOWEDORIGINS.split(',') : []; - -const corsOptions = { - origin: (origin, callback) => { - if (!origin || allowedOrigins.includes(origin)) { - callback(null, true); - } else { - callback(new Error('Not allowed by CORS')); - } - }, - optionsSuccessStatus: 200 -}; - -app.use(cors(corsOptions)); - -// Use the routes defined in routes.js -app.use("/", routes); - -app.listen(PORT, () => { - console.log("Server Listening on PORT:", PORT); -}); +const express = require("express"); +const cors = require("cors"); +const env = require("dotenv").config(); +const routes = require("./routes"); +const swaggerUi = require("swagger-ui-express"); +const swaggerDocument = require("./swagger.json"); +const bodyParser = require("body-parser"); + +const app = express(); +const PORT = process.env.PORT || 3000; + +app.use(express.json({ limit: '50mb' })); +app.use(bodyParser.json()); + +app.use(express.urlencoded({ limit: '50mb', extended: true })); + + +// Serve Swagger UI at `/api-docs` +app.use("/api-docs", swaggerUi.serve, swaggerUi.setup(swaggerDocument)); + +const allowedOrigins = process.env.ALLOWEDORIGINS ? process.env.ALLOWEDORIGINS.split(',') : []; + +const corsOptions = { + origin: (origin, callback) => { + if (!origin || allowedOrigins.includes(origin)) { + callback(null, true); + } else { + callback(new Error('Not allowed by CORS')); + } + }, + optionsSuccessStatus: 200 +}; + + +app.use(cors(corsOptions)); + +// Use the routes defined in routes.js +app.use("/", routes); + +app.listen(PORT, () => { + console.log("Server Listening on PORT:", PORT); +}); diff --git a/databindingsHandler.js b/databindingsHandler.js index 1a3a65e..f948dd3 100644 --- a/databindingsHandler.js +++ b/databindingsHandler.js @@ -5,7 +5,29 @@ const axios = require("axios"); const { getUsername, isUsernameValid } = require('./usernameHandler.js'); const { parse, format } = require("date-fns"); - +/** + * Custom Axios params serializer that encodes whitespace as %20 + */ +function customParamsSerializer(params) { + const queryParts = []; + for (const key in params) { + if (!Object.prototype.hasOwnProperty.call(params, key)) continue; + + const value = params[key]; + + if (value === null || typeof value === 'undefined') continue; + + // Handle arrays by repeating the key for each value + if (Array.isArray(value)) { + value.forEach((val) => { + queryParts.push(`${encodeURIComponent(key)}=${encodeURIComponent(val)}`); + }); + } else { + queryParts.push(`${encodeURIComponent(key)}=${encodeURIComponent(value)}`); + } + } + return queryParts.join('&'); +} async function populateDatabindings(formJson, params) { //start code here try { @@ -119,9 +141,10 @@ function bindDataToFields(formJson, fetchedData, params = {}) { function processItemsForDatabinding(items) { items.forEach(field => { + const fieldId = field?.id ? field.id : field?.uuid; // containers - if (field.type === 'container' && field.containerItems) { - return processItemsForDatabinding(field.containerItems); + if (field.type === 'container' && (field.containerItems || field.children)) { + return field.containerItems ? processItemsForDatabinding(field.containerItems) : processItemsForDatabinding(field.children); } // groups @@ -130,12 +153,12 @@ function bindDataToFields(formJson, fetchedData, params = {}) { // 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); + formData[fieldId] = bindRowsToGroup(field, rows); } else { // 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); + formData[fieldId] = bindRowsToGroup(field, rows); } return; } @@ -143,13 +166,15 @@ function bindDataToFields(formJson, fetchedData, params = {}) { //Single fields if (field.databindings) { const raw = getBindingValue(field.databindings, fetchedData, params); - formData[field.id] = raw != null ? transformValueToBindIfNeeded(field, raw) : null; + formData[fieldId] = raw != null ? transformValueToBindIfNeeded(field, raw) : null; } }); }; - if (Array.isArray(formJson?.data?.items)) { + if (Array.isArray(formJson?.data?.items)) { // Kiln-v1 processItemsForDatabinding(formJson.data.items); + } else if (Array.isArray(formJson?.elements)) { // Kilnv2 + processItemsForDatabinding(formJson.elements); } // console.dir(formData, { depth: null, colors: true }); return formData; @@ -192,12 +217,12 @@ async function readJsonFormApi(datasource, pathParams) { } if (type.toUpperCase() === 'GET') { // For GET requests, add params directly in axios config - response = await axios.get(url, { params: pathParams, headers } + response = await axios.get(url, { params: pathParams, headers, paramsSerializer: customParamsSerializer} ); } else if (type.toUpperCase() === 'POST') { // For POST requests, pass params in the body if applicable const bodyForPost = buildBodyWithParams(body, pathParams); - response = await axios.post(url, bodyForPost, { params: pathParams, headers }); + response = await axios.post(url, bodyForPost, { params: pathParams, headers, paramsSerializer: customParamsSerializer}); } // Store response data @@ -266,6 +291,16 @@ function transformValueToBindIfNeeded(field, valueToBind) { const transformedValue = format(parsedDate, "yyyy-MM-dd"); return transformedValue; } + if (field && field.type == "checkbox" && valueToBind){ + if (valueToBind == "Yes" || valueToBind=="yes") + { + return (true); + } + if (valueToBind == "No"|| valueToBind=="no") + { + return (false); + } + } } catch (error) { console.error('Error processing date value:', error); } @@ -302,7 +337,7 @@ function updateParams(params, pathParams = {}, allFetchedData = {}) { }); } - // 2. Replace all !!.[Source]=jsonpath in the string + // 2. Replace all '!!.[Source]=jsonpath' in the string if (typeof val === 'string') { val = val.replace(/'!!\.\[([^\]]+)\]=(.+\])'/g, (match, sourceName, jsonPath) => { const sourceData = allFetchedData[sourceName.trim()]; diff --git a/dateConverter.js b/dateConverter.js new file mode 100644 index 0000000..a5d36db --- /dev/null +++ b/dateConverter.js @@ -0,0 +1,21 @@ +/* Function changes string from YYYY-MM-DD date format to MM/DD/YYYY + * @param (String) originalDate + */ +function toICMFormat(originalDate) { + if (originalDate === '' || originalDate === null) { // If the date is empty, no need to proceed + return originalDate; + } + let newDate = ''; // The new date to be created + try { + const yearMonthDay = originalDate.split("-"); // Split into array [Year, Month, Day] + newDate = yearMonthDay[1] + "/" + yearMonthDay[2] + "/" + yearMonthDay[0] // Turn into "Month/Day/Year" + } catch (e) { + console.log("Something went wrong with configuring the date!"); + console.error(e); + return "-1"; + } + return newDate; +} + +// Export the function so it can be used in other files +module.exports = { toICMFormat }; \ No newline at end of file diff --git a/dictionary/dictionaryUtils.js b/dictionary/dictionaryUtils.js new file mode 100644 index 0000000..6f24c85 --- /dev/null +++ b/dictionary/dictionaryUtils.js @@ -0,0 +1,62 @@ +/** Checks if a dictionary key exists + * @param dictionary : the dictionary to search through + * @param key : the key in the dictionary + * @param property : the key's property + * @return boolean true/false + */ +function keyExists (dictionary, key) { + if (dictionary[key]) return true; + else return false; +}; + +/** Checks if a dictionary key has a property value + * @param dictionary : the dictionary to search through + * @param key : the key in the dictionary + * @param property : the key's property + * @return boolean true/false + */ +function propertyExists (dictionary, key, property) { + if (dictionary[key] && dictionary[key][property]) return true; + else return false; +}; + +/** Checks if a dictionary key's property's key exists + * @param dictionary : the dictionary to search through + * @param key : the key in the dictionary + * @param property : the key's property + * @param propertyKey : the property's key + * @return boolean true/false + */ +function propertyKeyExists (dictionary, key, property, propertyKey) { + if (dictionary[key] && dictionary[key][property] && dictionary[key][property][propertyKey]) return true; + else return false; +}; + +/** Checks if a property value is not empty + * @param dictionary : the dictionary to search through + * @param key : the key in the dictionary + * @param property : the key's property + * @return boolean true/false + */ +function propertyNotEmpty (dictionary, key, property) { + const propertyType = typeof dictionary[key][property]; + if (propertyType === "string" && dictionary[key][property] != "") return true; + else if (propertyType === "object" && dictionary[key][property].length != 0 && dictionary[key][property] != {}) return true; + else return false; +}; + +/** Checks if a property value is not empty + * @param dictionary : the dictionary to search through + * @param key : the key in the dictionary + * @param property : the key's property + * @param propertyKey : the property's key + * @return boolean true/false + */ +function propertyKeyNotEmpty (dictionary, key, property, propertyKey) { + const propertyType = typeof dictionary[key][property]; + if (propertyType === "string" && dictionary[key][property] != "") return true; + else if (propertyType === "object" && dictionary[key][property][propertyKey] != [] && dictionary[key][property][propertyKey].length != 0 && dictionary[key][property][propertyKey] != {}) return true; + else return false; +}; + +module.exports = { keyExists, propertyExists, propertyKeyExists, propertyNotEmpty, propertyKeyNotEmpty }; \ No newline at end of file diff --git a/dictionary/interfaces.js b/dictionary/interfaces.js new file mode 100644 index 0000000..8c24a5c --- /dev/null +++ b/dictionary/interfaces.js @@ -0,0 +1,249 @@ +/* List of interfaces based on server config + */ +const interfaces = { + NETportals: { + interface: [ + { + type: "button", + label: "Submit", + mode: ["portalEdit"], + style: "", + actions: [ + { + action_type: "javascript", + script: `if (!validateAllFields()) + { setModalTitle("Validation Error"); + setModalMessage("Please clear the errors in the form before submitting."); + setPrimaryButton(""); + setSecondaryButton(""); + setModalOpen(true); + return false; }` + }, + { + action_type: "javascript", + script: `const confirmed = await confirmModal(); + if (!confirmed) return false;` + }, + { + action_type: "endpoint", + api_path: "API.saveButtonAction", + body: "tokenId: params[\"id\"],savedForm: JSON.stringify(createSavedData())", + type: "POST" + }, + { + action_type: "endpoint", + api_path: "API.submitButtonAction", + body: "tokenId: params[\"id\"]", + type: "POST" + } + ,{ + action_type: "javascript", + script: `await handleSubmit();` + } + ] + }, + { + type: "button", + label: "Print", + mode: ["portalView"], + style: "", + actions: [ + { + action_type: "javascript", + script: `handlePrint();` + }] + }, + { + type: "button", + label: "Cancel", + mode: ["portalEdit","portalView"], + style: "", + actions: [ + { + action_type: "endpoint", + api_path: "API.cancelButtonAction", + body: `tokenId: params["id"]`, + type: "POST" + }, + { + action_type: "javascript", + script: `await handleCancel();` + }] + }, + + + ] + }, + NETportalsV2: { + interface: [ + { + type: "button", + label: "Submit", + mode: ["portalEdit"], + style: "primary", + actions: [ + { + "action_type": "javascript", + "script": "if (!validateAllFields()?.isValid) { setModalTitle('Validation Error'); setModalMessage('Please fix the highlighted fields.'); setModalOpen(true); return false; }" + }, + { + "action_type": "javascript", + "script": "const confirmed = await confirmModal(); if (!confirmed) { return false; }" + }, + { + action_type: "endpoint", + api_path: "API.saveButtonAction", + type: "POST", + body: "tokenId: params['id'], savedForm: JSON.stringify(createSavedData())" + }, + { + action_type: "endpoint", + api_path: "API.submitButtonAction", + type: "POST", + body: "tokenId: params['id']" + }, + { + action_type: "javascript", + script: `await handleSubmit();` + } + ] + }, + { + type: "button", + label: "Cancel", + mode: ["portalEdit", "portalView"], + style: "secondary", + actions: [ + { + action_type: "endpoint", + api_path: "API.cancelButtonAction", + type: "POST", + body: "tokenId: params['id']", + }, + { + action_type: "javascript", + script: "await handleCancel();" + } + ] + }, + { + type: "button", + label: "Print", + mode: ["portalView"], + style: "tertiary", + actions: [ + { + action_type: "javascript", + script: "handlePrint();" + } + ] + } + ] + }, + CAREGIVER: { + interface: [ + { + mode: [ + "portalNew", + "portalEdit" + ], + type: "button", + label: "Save", + style: "", + actions: [ + { + script: "setFormErrors({});if (!validateAllFields()){ window.parent.postMessage(JSON.stringify({ \"event\": \"errorOnSave\" }), \"*\"); return true; }", + action_type: "javascript" + }, + { + body: "tokenId: params[\"id\"],savedForm: JSON.stringify(createSavedData())", + path: "/application-forms/saveDraft", + type: "POST", + api_path: "API.saveButtonAction", + action_type: "endpoint" + } + ] + }, + { + mode: [ + "portalNew", + "portalEdit" + ], + type: "button", + label: "Complete", + style: "", + actions: [ + { + script: "setFormErrors({});if (!validateAllFields()){ window.parent.postMessage(JSON.stringify({ \"event\": \"errorOnComplete\" }), \"*\"); return false; }", + action_type: "javascript" + }, + { + body: "tokenId: params[\"id\"],savedForm: JSON.stringify(createSavedData())", + path: "/application-forms/submit", + type: "POST", + api_path: "API.saveButtonAction", + action_type: "endpoint" + }, + { + script: "await handleSubmit();", + action_type: "javascript" + } + ] + } + ] + }, + CAREGIVERV2: { + interface: [ + { + mode: [ + "portalNew", + "portalEdit" + ], + type: "button", + label: "Save", + style: "", + actions: [ + { + script: "if (!validateAllFields()?.isValid){ window.parent.postMessage(JSON.stringify({ \"event\": \"errorOnSave\" }), \"*\");return true; }", + action_type: "javascript" + }, + { + body: "tokenId: params['id'], savedForm: JSON.stringify(createSavedData())", + path: "/application-forms/saveDraft", + type: "POST", + api_path: "API.saveButtonAction", + action_type: "endpoint" + } + ] + }, + { + mode: [ + "portalNew", + "portalEdit" + ], + type: "button", + label: "Complete", + style: "", + actions: [ + { + script: "if (!validateAllFields()?.isValid){ window.parent.postMessage(JSON.stringify({ \"event\": \"errorOnComplete\" }), \"*\"); return false; }", + action_type: "javascript" + }, + { + body: "tokenId: params['id'], savedForm: JSON.stringify(createSavedData())", + path: "/application-forms/submit", + type: "POST", + api_path: "API.saveButtonAction", + action_type: "endpoint" + }, + { + script: `await handleSubmit();`, + action_type: "javascript" + } + ] + } + ] + } +}; + +module.exports = { interfaces }; \ No newline at end of file diff --git a/dictionary/jsonXmlConversion.js b/dictionary/jsonXmlConversion.js new file mode 100644 index 0000000..839ada0 --- /dev/null +++ b/dictionary/jsonXmlConversion.js @@ -0,0 +1,1412 @@ +/* List of forms that do not convert directly from JSON to XML + * Keys are the form_definition.form_id + * Property: rootName : a string that will replace "root" from the XML + * Property: subRoots : an array of strings. These will be between root and JSON data. The first value in the array will be highest (after root) while the last will be lowest (before JSON) + * Property: wrapperTags : an array of objects. If not empty, then should include object { [the wrapper Tag name] : { wrapFieldsName: indexOfDepth } } + * Property: allowCheckboxWithNoChange : if a field is in this array, skip conversion step + * Property: omitFields : an array of strings. These are the UUIDs of fields that must be omitted before XML creation. For child UUIDs, include only the values after "-i-" where i is the index of the list + * Property: version : a string that will check if the exception list should include the version of the form submitted. All values in version should overwrite the default form properties. + */ +const formExceptions = { + "CF0630": { + "rootName": "ListOfDtFormInstanceLw", + "subRoots": ["FormInstance"], + "wrapperTags": [], + "allowCheckboxWithNoChange": [], + "omitFields": [], + "addFields": {}, + "versions": { + "1": { + "omitFields": [] + }, + "2": { + "omitFields": [] + } + } + }, + "CF0631": { + "rootName": "ListOfDtFormInstanceLw", + "subRoots": ["FormInstance"], + "wrapperTags": [], + "allowCheckboxWithNoChange": [], + "omitFields": [], + "addFields": {}, + "versions": { + "1": { + "omitFields": [] + }, + "2": { + "omitFields": [] + } + } + }, + "CF0632": { + "rootName": "ListOfDtFormInstanceLw", + "subRoots": ["FormInstance"], + "wrapperTags": [], + "allowCheckboxWithNoChange": [], + "omitFields": [], + "addFields": {}, + "versions": { + "1": { + "omitFields": [] + }, + "2": { + "omitFields": [] + } + } + }, + "CF0633": { + "rootName": "ListOfDtFormInstanceLw", + "subRoots": ["FormInstance"], + "wrapperTags": [ + { + "ListOfTeamLeads": { + "TeamLead": { + "team-leader-name-25e284ba-5eaf-4b67-afb0-9d05e7214669": 1 + } + }, + "ListOfOffice": { + "Office": { + "office-code-a7c2501c-d181-4b1a-a44e-1036e4f1f77b": 1 + } + } + } + ], + "allowCheckboxWithNoChange": [], + "omitFields": [], + "addFields": { + "ListOfUserEnteredData9": { + "UserEnteredData9": { + "String35": null, + "String34": null, + "String33": null, + "Date1": null + } + }, + "ListOfUserEnteredData8": { + "UserEnteredData8": { + "String42": null, + "String41": null + } + }, + "ListOfUserEnteredData12": { + "UserEnteredData12": { + "String42": null, + "String41": null, + "String36": null, + "Date9": null + } + }, + "HeaderERIQ": null, + "Created": null, + "FormInstanceId": null, + "CreatedBy": null, + "CaseContactERIQ": null, + "SRNum": null, + "BenefitPlanId": null, + "CaseId": null, + "Category": null, + "ContactId": null, + "DocFileName": null, + "DocFileSize": null, + "DocFileSrcType": null, + "FinalFlag": null, + "ICPId": null, + "IncidentNo": null, + "MISCaseNum": null, + "CaseNum": null, + "SRId": null, + "SubCategory": null, + "Template": null, + "RenderFlatFlag": null, + "Bool01": null, + "Bool02": null, + "Bool03": null, + "Bool04": null, + "Bool05": null, + "TemplateLocation": null, + "Date01": null, + "Date03": null, + "Date04": null, + "Date05": null, + "String01": null, + "String02": null, + "String03": null, + "String04": null, + "String05": null, + "String06": null, + "String07": null, + "String08": null, + "String09": null, + "String10": null, + "String11": null, + "AuxName": null, + "AuxType": null, + "Number01": null, + "Number02": null, + "Number03": null, + "Number04": null, + "Number05": null, + "Bool06": null, + "Bool07": null, + "Bool08": null, + "Bool09": null, + "Bool010": null, + "Date06": null, + "Date07": null, + "Bool11": null, + "Bool12": null, + "Bool13": null, + "Bool14": null, + "Bool15": null, + "Bool16": null, + "Bool17": null, + "Bool18": null, + "Bool19": null, + "Bool20": null, + "Bool21": null, + "Bool22": null, + "Bool23": null, + "Bool24": null, + "Bool25": null, + "Bool26": null, + "Bool27": null, + "Bool28": null, + "Bool29": null, + "Bool30": null, + "Bool31": null, + "Bool32": null, + "Bool33": null, + "Bool34": null, + "Bool35": null, + "Bool36": null, + "Bool37": null, + "Bool38": null, + "Bool39": null, + "Bool40": null, + "Bool41": null, + "Bool42": null, + "Bool43": null, + "Bool44": null, + "Bool45": null, + "Bool46": null, + "Bool47": null, + "Bool48": null, + "Bool49": null, + "Bool50": null, + "String14": null, + "String15": null, + "String16": null, + "String17": null, + "String18": null, + "String19": null, + "String20": null, + "String21": null, + "String22": null, + "String23": null, + "String24": null, + "String25": null, + "String26": null, + "String27": null, + "String28": null, + "String29": null, + "String30": null, + "String31": null, + "String32": null, + "String33": null, + "String34": null, + "String35": null, + "String36": null, + "String37": null, + "String38": null, + "String39": null, + "String40": null, + "String41": null, + "String42": null, + "String43": null, + "String44": null, + "String45": null, + "String46": null, + "String47": null, + "String48": null, + "String49": null, + "String50": null, + "String51": null, + "String52": null, + "String53": null, + "String54": null, + "String55": null, + "String56": null, + "String57": null, + "String58": null, + "String59": null, + "String60": null, + "String61": null, + "String62": null, + "String63": null, + "String64": null, + "String65": null, + "ListOfContact": { + "Contact": { + "BirthDate": null, + "CaseRelTypeCode": null, + "ClientIDNumber": null, + "EmailAddress": null, + "FirstName": null, + "FullName": null, + "FullNameMiddle": null, + "LastName": null, + "MF": null, + "MiddleName": null, + "MiddleNameInitial": null + } + }, + "ListOfCase": { + "Case": { + "CaseNum": null, + "Name": null, + "Status": null, + "ListOfContactAbove18": { + "ContactAbove18": { + "ContactFullName": null, + "ContactFullNameMiddle": null, + "AddressLine2": null, + "Province": null, + "AddressLine1": null, + "City": null, + "AddressLine3": null, + "PostalCode": null, + "ClientIDNumber": null, + "MiddleNameInitial": null, + "PersonUId": null + } + }, + "ListOfContactBelow18": { + "ContactBelow18": { + "AddressLine1": null, + "AddressLine2": null, + "City": null, + "PostalCode": null, + "AddressLine3": null, + "ContactFullName": null, + "Province": null, + "ContactFullNameMiddle": null, + "Gender": null, + "ContactId": null, + "Age": null, + "BirthDate": null, + "FirstName": null, + "Role": null, + "ClientIDNumber": null, + "MiddleName": null, + "Relationship": null, + "MiddleNameInital": null, + "LastName": null, + "PersonUId": null, + "Date01": null, + "PersonIdICM": null, + "Number02": null + } + } + } + }, + "ListOfEmployee": { + "Employee": { + "EMailAddr": null, + "Emp": null, + "Fax": null, + "FirstName": null, + "FullName": null, + "FullNameMiddle": null, + "JobTitle": null, + "LastName": null, + "MiddleName": null, + "MiddleNameInitial": null, + "WorkPhoneNumber": null + } + }, + "ListOfDtFormInstanceChildData": { + "DtFormInstanceChildData": { + "Date01": null, + "Date02": null, + "Number01": null, + "Number02": null, + "Number03": null, + "Number04": null, + "String01": null, + "String02": null + } + }, + "ListOfDtFormInstanceChildData2": { + "DtFormInstanceChildData2": { + "Bool01": null, + "Date01": null, + "Number05": null, + "String01": null, + "String02": null + } + }, + "ListOfDtFormInstanceChildData3": { + "DtFormInstanceChildData3": { + "Bool03": null, + "String01": null, + "String02": null, + "String03": null, + "String04": null, + "String05": null, + "String07": null, + "String08": null, + "String09": null + } + }, + "ListOfDtFormInstanceChildData4": { + "DtFormInstanceChildData4": { + "Bool01": null + } + }, + "ListOfDtFormInstanceChildData11": { + "DtFormInstanceChildData11": { + "Bool01": null, + "Date01": null, + "Date02": null, + "String01": null, + "String02": null + } + }, + "ListOfDtFormInstanceChildData5": { + "DtFormInstanceChildData5": { + "String12": null, + "String13": null, + "String14": null + } + } + }, + "versions": { + "1": { + "omitFields": [] + }, + "2": { + "omitFields": [] + } + } + }, + "CF0640" : { + "rootName": "ListOfDtFormInstanceLw", + "subRoots": ["FormInstance"], + "wrapperTags": [], + "allowCheckboxWithNoChange": [], + "omitFields": [], + "addFields": { + "ListOfPUBLead": { + "PUBLead": { + "LeadID": null, + "Name": null, + "ListOfICMCPScreeningAssessment": { + "ICMCPScreeningAssessment": { + "DateApproved": null, + "GeneralNeglectC": null, + "GeneralNeglectFlag": null, + "Neglect": null, + "OverrideNPRA": null, + "OverrideNPRB": null, + "OverridePRA": null, + "OverrideReadOnlyCalc": null, + "Phone": null, + "PhysicalAbuseF": null, + "Position": null, + "ResponseNameExplanation": null, + "SevereNeglectFlag": null, + "SexualAbuseC": null, + "TLSummary": null, + "TeamLeaderPositionId": null + } + } + } + }, + "FormInstanceId": null, + "SRNum": null, + "CaseId": null, + "IncidentNo": null, + "SRId": null, + "Template": null, + "TemplateLocation": null + }, + "versions": { + "1": { + "omitFields": [] + }, + "2": { + "omitFields": [] + } + } + }, + "CF0925": { + "rootName": "form1", + "subRoots": [], + "wrapperTags": [ + { + "ParentGuardian": { + "last-name-91433118-4a0c-47fe-80d8-2676f83a2022": 0, + "first-name-5dd5f94c-be98-4dd1-a442-3903c6330391": 0, + "middle-names-690ef4b4-bf83-487e-8ef2-fd39f09fb545": 0, + "address-parent-9dcf849c-b28b-4faa-afe3-8603af7c98fa": 0, + "unit-fbbdce5c-a970-4d8f-98f5-d3a6e8af6005": 0, + "address-1-a81293da-3d95-42cf-b6fa-ec15fac39ed1": 0, + "address-2-3f5515c8-8d91-424f-8302-0e7fea6599bf": 0, + "city-80cbf0f7-fa28-47f0-a4e4-94793e8a12fb": 0, + "province-835a46e3-4a7c-4496-a196-8e93172092ed": 0, + "postal-code-29edf972-2b5c-421d-a1ab-d97aa4005911": 0, + "primary-phone-number-type-a7eba596-6474-4a01-93e5-6cd49c8ef4cc": 0, + "primary-phone-number-f29b9ffd-9e7c-4f20-8bc3-57b333d88a0e": 0, + "secondary-phone-number-type-fc15a5f0-bfcd-4fe8-8836-833fc2573525": 0, + "secondary-phone-number-135375c3-f0bc-4b3a-aea2-b98995676ebc": 0 + } + }, + { + "Child" : { + "child-last-name-15a0adbf-a3bd-4bd1-be3f-bfb9a2b02f3e": 0, + "child-first-name-4a8302bb-6eca-46c8-ae50-efe37434ea8e": 0, + "child-middle-names-81cf1856-1541-4eed-85d8-1f376b727fd0": 0, + "child-dob-3d6f9a94-7c0f-4a06-89dc-872ee39fa818": 0 + } + }, + { + "PartB" : { + "sub-total-equipment-and-supplies-96a6e253-7a5d-4c7e-b2f4-abe26d48c478": 0, + "sub-total-training-ff57ad93-2e67-496e-aace-830303b0172e": 0, + "total-tte-e9836886-e4c3-49cf-83c8-c708dd005ab7": 0, + "eligible-total-tte-8de84d25-c659-4b7d-91b8-e16f99e44a29": 0, + "i-agree-that-these-expenses-are-related-to-my-childs-autism-intervention-52374221-28ad-4214-ad55-24c731900c21": 0, + "equipment-and-supplies-expense-605f2d0c-41d7-412d-9eff-6786964e5c6f": 0, + "TravelList": { + "Travel": { + "reason-for-travel-da2832f4-b848-4601-bd2f-2d796bf085c8": 2, + "start-date-of-trip-719d8dd1-fbb9-422c-afa8-2aaacaf61ccb": 2, + "end-date-of-trip-11639718-ab5b-40a6-a724-b89aecbc4a62" : 2, + "name-of-travellers-67f591a6-8f2c-4242-9653-49c6b95ae9e2": 2, + "from-location-5b51204f-e85c-4846-8c12-38373df07ef4": 2, + "to-location-52571198-2842-41b9-aec6-3e4f4bb23599": 2, + "travel-expense-68d80d8a-ed7f-44cc-8d85-74b10d62d7d8": 2 + } + }, + "training-expense-6452d421-896d-4ae3-9588-621fca63151f": 0, + "sub-total-travel-e78551b9-35f1-40d5-8e7e-8ef00637753d": 0, + "eligible-sub-total-travel-bf11f98e-d39f-4a67-a1a2-a0b6b36a7440": 0, + } + }, + { + "InterventionService": { + "start-date-intervention-e2cda870-27fc-4b35-876d-0af737e13ea0": 0, + "end-date-intervention-13687d44-ebfc-419e-8fa0-238d7ef29836": 0, + "total-intervention-54d8d0df-9a34-4474-9e34-077a9bb3b3ff": 0, + "eligible-total-intervention-751659e6-5f37-41ab-b179-21c22e2292a5": 0, + "intervention-expense-77e6cc72-66f9-4b6d-a437-cf64b7017b1f": 0, + "funding-period-start-dc5ab528-454b-4746-a556-a2591ff3967b": 0, + "funding-period-end-date-b47975a4-2d33-4578-9c00-ce42da63a99e": 0 + } + } + ], + "allowCheckboxWithNoChange": [], + "omitFields": [], + "addFields": { + "SRSubType": null, + "ParentGuardian": { + "ApplicantMailingAddress": null + } + }, + "versions" : { + "1" :{ + "omitFields": [] + }, + "2" : { + "omitFields": [] + } + } + }, + "CF0926": { + "rootName": "form1", + "subRoots": [], + "wrapperTags": [], + "allowCheckboxWithNoChange": [], + "omitFields": [], + "addFields": {}, + "versions": { + "1": { + "omitFields": [] + }, + "2": { + "omitFields": [] + } + } + }, + "CF1070": { + "rootName": "ListOfDtFormInstanceLw", + "subRoots": ["FormInstance"], + "wrapperTags": [], + "allowCheckboxWithNoChange": [], + "omitFields": [], + "addFields": { + "Created": null, + "FormInstanceId": null, + "CreatedBy": null, + "SRNum": null, + "BenefitPlanId": null, + "CaseId": null, + "Category": null, + "DocFileName": null, + "DocFileSize": null, + "DocFileSrcType": null, + "FinalFlag": null, + "MISCaseNum": null, + "CaseNum": null, + "SRId": null, + "SubCategory": null, + "Template": null, + "RenderFlatFlag": null, + "Bool01": null, + "Bool02": null, + "Bool03": null, + "Bool04": null, + "Bool05": null, + "TemplateLocation": null, + "Date01": null, + "Date02": null, + "Date03": null, + "Date04": null, + "Date05": null, + "String01": null, + "String02": null, + "String03": null, + "String04": null, + "String05": null, + "String06": null, + "String07": null, + "String08": null, + "String09": null, + "String10": null, + "String11": null, + "String12": null, + "AuxName": null, + "AuxType": null, + "Number01": null, + "Number02": null, + "Number03": null, + "Number04": null, + "Number05": null, + "Bool06": null, + "Bool07": null, + "Bool08": null, + "Bool09": null, + "Bool10": null, + "Date06": null, + "Date07": null, + "Bool11": null, + "Bool12": null, + "Bool13": null, + "Bool14": null, + "Bool15": null, + "Bool16": null, + "Bool17": null, + "Bool18": null, + "Bool19": null, + "Bool20": null, + "Bool21": null, + "Bool22": null, + "Bool23": null, + "Bool24": null, + "Bool25": null, + "Bool26": null, + "Bool27": null, + "Bool28": null, + "Bool29": null, + "Bool30": null, + "Bool31": null, + "Bool32": null, + "Bool33": null, + "Bool34": null, + "Bool35": null, + "Bool36": null, + "Bool37": null, + "Bool38": null, + "Bool39": null, + "Bool40": null, + "Bool41": null, + "Bool42": null, + "Bool43": null, + "Bool44": null, + "Bool45": null, + "Bool46": null, + "Bool47": null, + "Bool48": null, + "Bool49": null, + "Bool50": null, + "String13": null, + "String14": null, + "String15": null, + "String16": null, + "String17": null, + "String18": null, + "String19": null, + "String20": null, + "String21": null, + "String22": null, + "String23": null, + "String24": null, + "String25": null, + "String26": null, + "String27": null, + "String28": null, + "String29": null, + "String30": null, + "String31": null, + "String32": null, + "String33": null, + "String34": null, + "String35": null, + "String36": null, + "String37": null, + "String38": null, + "String39": null, + "String40": null, + "String41": null, + "String42": null, + "String43": null, + "String44": null, + "String45": null, + "String46": null, + "String47": null, + "String48": null, + "String49": null, + "String50": null, + "String51": null, + "String52": null, + "String53": null, + "String54": null, + "String55": null, + "String56": null, + "String57": null, + "String58": null, + "String59": null, + "String60": null, + "String61": null, + "String62": null, + "String63": null, + "String64": null, + "String65": null, + "ListOfEmployee": { + "Employee": { + "EMailAddr": null, + "Emp": null, + "Fax": null, + "FirstName": null, + "FullName": null, + "FullNameMiddle": null, + "JobTitle": null, + "LastName": null, + "MiddleName": null, + "MiddleNameInitial": null, + "WorkPhoneNumber": null + } + }, + "ListOfTeamLeads": { + "TeamLead": { + "EMailAddr": null, + "Emp": null, + "Fax": null, + "FirstName": null, + "FullName": null, + "FullNameMiddle": null, + "JobTitle": null, + "LastName": null, + "MiddleName": null, + "MiddleNameInitial": null, + "WorkPhoneNumber": null + } + }, + "ListOfOffice": { + "Office": { + "OfficeName": null, + "OfficeRegion": null, + "OfficeAddressLine1": null, + "OfficeAddressLine2": null, + "OfficeAddressLine3": null, + "OfficeAddressCity": null, + "OfficeAddressProvince": null, + "OfficeAddressCountry": null, + "OfficeAddressPostalCode": null, + "OfficeAddressUnit": null, + "OfficePhone": null, + "OfficeFax": null, + "OfficeAddressComplete": null, + "OfficeAddressCompleteCity": null + } + }, + "ListOfPubHlsIncident": { + "PubHlsIncident": { + "ListOfIcmIncidentSafetyAssessmentBc": { + "IcmIncidentSafetyAssessmentBc": { + "ApprovedtoFinalizeDate": null, + "FinalizedBy": null, + "FinalizedDate": null, + "SafetyFactorA": null, + "AssessmentId": null + } + }, + "IncidentCity": null, + "IncidentLocation": null, + "IncidentPostalCode": null, + "Location": null, + "Name": null, + "ListOfIncidentContactBelow18": { + "IncidentContactBelow18": { + "ContactFullName": null, + "ContactFullNameMiddle": null, + "MiddleNameInitial": null, + "MiddleName": null, + "Number01": null, + "Number03": null + } + } + } + } + }, + "versions": { + "1": { + "omitFields": [] + }, + "2": { + "omitFields": [] + } + } + }, + "CF2900": { + "rootName": "ListOfDtFormInstanceLw", + "subRoots": [], + "wrapperTags": [ + { + "ListofDependent": { + "Dependent": { + "CareArrangementsList": { + "CareArrangements": { + "licensed-group-b650d0a6-2871-433c-9a2b-9e80a2f7c9a8": 3, + } + }, + "first-name-4f1d33c2-cd25-4801-9902-d1d33a0ba010": 1, + "middle-name-9db65275-66a0-4f3d-856b-9cd9ded0f385": 1, + "last-name-dc75eb1e-d57d-4383-a9e6-5e0a86513700": 1, + "date-of-birth-yyyy-mmm-dd-0e756b1c-47c9-482b-82fa-9c681c87ea55": 1, + "radiobuttonlist-d7ada287-f4af-4a1a-ad9f-daa893b8789f": 1, + "radiobuttonlist-bdbd4d0f-daf0-445e-abb0-4eb42f3e15c0": 1, + "licensed-group-b650d0a6-2871-433c-9a2b-9e80a2f7c9a8": 1, + "licensed-family-f14e4cd1-204e-41a7-b1b2-d7b910cc2ee4": 1, + "licensed-preschool-program-15821c42-4a1f-4fc4-98e0-6a0fedc6cf91": 1, + "registered-licence-not-required-455d1970-fe3a-4d7c-9ca0-d365f1177fb1": 1, + "licence-not-required-a0b0868e-c7e0-43d3-b688-d2569108c595": 1, + "in-childs-home-2b8a7f32-e472-4cad-aba5-254b33125844": 1, + "radiobuttonlist-28b592b7-aba8-46dc-8caf-815b8684fd31": 1, + "custody-details-e38e1e1a-4db9-460d-bde3-a9b626419173": 1 + } + } + }, + { + "ListReasonForCare": { + "ReasonForCare": { + "name-of-employers-school-training-program-or-dates-looking-for-work-3bb24589-196e-4a3b-bf9d-0b2616efec89": 1, + "start-date-yyyy-mmm-dd-a745b70e-8304-4814-be69-1b232c44beeb": 1, + "end-date-yyyy-mmm-dd-d2df073a-101d-4a68-bf42-1a040ebf7585": 1, + "week-days-andor-week-ends-317392fa-4395-401a-b9ec-fd60930177f1": 1, + "week-end-days-per-week-8baf2547-c051-4fc5-849a-5aa527fb326a": 1, + "week-end-hours-per-day-3afabfee-a210-4465-80e3-788bae3d59d4": 1, + "week-day-days-per-week-3176c7d7-751b-4753-871c-d53bf6128e3e": 1, + "week-day-hours-per-day-00303def-6fad-49d6-b72b-183abef2fbd5": 1, + "regular-schedule-73687eed-2e55-4aa7-821a-deab607da5d3": 1, + "start-time-0187c5ad-c8b0-420b-bded-595d2ecad5bd": 1, + "end-time-d7f384c0-f80f-4aa3-9430-7a86187897df": 1, + "additional-information-or-attach-a-schedule-77a4d493-0468-40a7-8d0e-af9ab9906085": 1, + "travel-time-c1ab5674-a91d-4ab2-b096-213b07bdb69d": 1 + } + } + } + ], + "allowCheckboxWithNoChange": [], + "omitFields": [], + "addFields": { + "AlternateIncomePresent": null, + "ICMAssistance": null, + "SpousalConsent": null, + "SpouseAdditionalIncomeReported": null, + "SpouseAlternateIncomePresent": null, + "ApplicantSocialWorkerInvolvement": null, + "ICMApplicantDateofBirth": null, + "ApplicantGender": null, + "ApplicantPrimaryPhoneNumberType": null, + "ApplicantHomeApartment": null, + "ApplicantHomeMAKID": null, + "ApplicantMailingApartment": null, + "ApplicantEmail": null, + "CareArrangementUploaded": null, + "ICMSpouseDateofBirth": null, + "SpouseGender": null, + "ListofDependent": { + "Dependent": { + "CareArrangementsList": { + "CareArrangements": { + "LicenceNumber": null, + "MailingAddress": null, + "MailingCity": null, + "MailingPostalCode": null, + "CCAProviderComments": null, + "FacilityID": null, + "CCAProviderName": null, + "CCAProviderDaytimePhone": null, + "CCAProviderSecondaryPhone": null, + "FacilityName": null, + "ServiceAddress": null, + "ServiceCity": null, + "ServicePostalCode": null, + "CCACareType": null, + "SundayFlag": null, + "SaturdayFlag": null, + "CCASupplierNumber": null, + "CCAStartDate": null, + "CCAEndDate": null, + "MonthlyFee": null, + "DailyFee": null, + "SchoolClosureFee": null, + "EnrolledYN": null, + "SummerFlag": null, + "CCAChildRelatedFlag": null, + "CCAChildHomeFlag": null, + "CCARelativeFlag": null, + "CCARelationship": null, + "CCASameHomeFlag": null, + "CCAParentComments": null, + "ParentSumissionDate": null, + "ProviderSubmissionDate": null, + "ProviderBCeID": null, + "MondayFlag": null, + "MondayTime1Start": null, + "MondayTime1End": null, + "TuesdayFlag": null, + "TuesdayStart1Time": null, + "TuesdayEnd1Time": null, + "WednesdayFlag": null, + "WednesdayStart1Time": null, + "WednesdayEnd1Time": null, + "ThursdayFlag": null, + "ThursdayStart1Time": null, + "ThursdayEnd1Time": null, + "FridayFlag": null, + "FridayStart1Time": null, + "FridayEnd1Time": null, + "ProviderOrLicenseeName": null, + "FirstTimeApplyingFlag": null, + "ReplacingProviderFlag": null, + "ReplacingProviderName": null, + "AdditionalProviderFlag": null, + "OtherProviderName": null + } + }, + "ICMDependantDateofBirth": null, + "DependantGender": null, + "DependantMinistryPlacement": null, + "ICMDependentCount": null + } + }, + "ListofIncome": { + "AdditionalIncomeReported": null + }, + "ListReasonForCare": { + "ReasonForCare": { + "Role": null, + "CareType": null, + "ICMTravelTime": null, + "ICMStartDate": null, + "ICMCareType": null + } + }, + "ICMBCFlag": null, + "ICMFerderalFlagSpouse": null, + "ICMFederalFlag": null, + "Field1": null, + "ICMSpouseGUID": null, + "CareArrangementExists": null, + "CareArrangementDetailsPresent": null, + "CRAFiled": null, + "IncomeSameSinceTaxFiling": null, + "SpouseCRAFiled": null, + "SpouseIncomeSameSinceTaxFiling": null + }, + "versions": { + "1": { + "omitFields": [] + }, + "2": { + "omitFields": [] + } + } + }, + "HR0080": { + "rootName": "Results", + "subRoots": [], + "wrapperTags": [], + "allowCheckboxWithNoChange": [], + "omitFields": [], + "addFields": { + "OfficeCity": null, + "FormInstanceId": null, + "Sanctioned": null, + "CanadianCitizen": null, + "ApplicantTwoYearIndependence": null, + "ApplicantTwoYearRationale": null, + "SpouseTwoYearIndependence": null, + "SpouseTwoYearRationale": null + }, + "versions": { + "1": { + "omitFields": [] + }, + "2": { + "omitFields": [] + } + } + }, + "HR0080R": { + "rootName": "Replacement", + "subRoots": ["FormInstance"], + "wrapperTags": [], + "allowCheckboxWithNoChange": [], + "omitFields": [], + "addFields": { + "FormInstanceId": null, + "CaseWorkerId": null, + "SRNum": null, + "CaseId": null, + "Category": null, + "CaseOfficeAddrCity": null, + "CaseOfficeAddrCountry": null, + "CaseOfficeAddrLine1": null, + "CaseOfficeAddrLine2": null, + "CaseOfficeAddrLine3": null, + "CaseOfficeAddrPostalCode": null, + "CaseOfficeAddrProvince": null, + "CaseType": null, + "SRId": null, + "Template": null, + "TemplateLocation": null, + "CaseName": null, + "CaseOfficeAddrComplete": null, + "CaseOfficeAddrCompleteCity": null, + "ListOfContact": { + "Contact":{ + "CaseRelTypeCode": null, + "EmailAddress": null, + "PHN": null, + "PersonUId": null + } + }, + "ListOfCase": { + "Case": { + "ListOfICMSSAAExpenseVBC": { + "ICMSSAAExpenseVBC": { + "CaseId": null, + "CaseRelTypeCode": null, + "PhoneBasicRate": null, + "RoomandBoardpaidtofamily": null + } + }, + "CaseNum": null, + "ClosedDate": null, + "Name": null, + "Status": null, + "Type": null, + "LastName2": null, + "CloseReason": null, + "LegacyFileNumber": null, + "ReopenedDate": null, + "ListOfApplicant": { + "Applicant": { + "ListOfICMSSAAApplicantAdditionalIncome": { + "ICMSSAAApplicantAdditionalIncome": { + } + }, + "ListOfICMSSAAApplicantIncome": { + "ICMSSAAApplicantIncome": { + "Boarder": null, + "SupportMaintenance": null, + "WorkersCompensation": null, + "EmploymentInsurance": null, + "ExtendedSpousesAllowance": null, + "CPPQPPOverride": null, + "CaseId": null, + "InterestDividends": null + + } + } + + }, + "ContactFullName": null, + "Age": null, + "Role": null, + "PHN": null, + "MovedFrmProvince": null, + "SelfIdentified": null, + "YRIndMet": null, + "DateSeparated": null, + "PossiblyAboriginal": null, + "AboriginalOriginCode": null, + "AboriginBand": null, + "DivorcedDate": null, + "ContactFullNameMiddle": null + }, + "ListOfSpouse": { + "Spouse": { + "ListOfICMSSAASpouseAdditionalIncome": { + "ICMSSAASpouseAdditionalIncome": { + + } + }, + "ListOfICMSSAASpouseIncome": { + "ICMSSAASpouseIncome": { + "WorkersCompensation": null, + "ExtendedSpousesAllowance": null, + "CPPQPPOverride": null, + "CaseId": null, + "InterestDividends": null + + } + }, + "ContactFullName": null, + "Gender": null, + "Age": null, + "Role": null, + "PHN": null, + "HomePhone": null, + "MovedFrmProvince": null, + "SelfIdentified": null, + "AboriginBand": null, + "AboriginalOriginCode": null, + "WorkPhone": null, + "ImmigrationEntryType": null, + "CurrentImmigrationType": null, + "YRIndMet": null, + "DateSeparated": null, + "PossiblyAboriginal": null, + "ContactFullNameMiddle": null, + + } + }, + "ListOfDependent": null, + "ListOfOccupant": null, + "ListOfICMCaseAddress": { + "ICMCaseAddress": { + "Country": null, + "CompleteAddressCity": null, + + } + } + } + } + }, + "versions": { + "1": { + "omitFields": [] + }, + "2": { + "omitFields": [] + } + } + }, + "HR3687E": { + "rootName": "ListOfDtFormInstanceLw", + "subRoots": ["FormInstance"], + "wrapperTags": [], + "allowCheckboxWithNoChange": [], + "omitFields": [], + "addFields": { + "FormInstanceId": null, + "CreatedBy": null, + "SRNum": null, + "BenefitPlanId": null, + "CaseId": null, + "Category": null, + "ContactId": null, + "DocFileName": null, + "DocFileSize": null, + "DocFileSrcType": null, + "FinalFlag": null, + "ICPId": null, + "MISCaseNum": null, + "SRId": null, + "SubCategory": null, + "Template": null, + "RenderFlatFlag": null, + "Bool01": null, + "Bool02": null, + "Bool03": null, + "Bool04": null, + "Bool05": null, + "TemplateLocation": null, + "Date01": null, + "Date02": null, + "Date03": null, + "Date04": null, + "Date05": null, + "String01": null, + "String02": null, + "String03": null, + "String04": null, + "String05": null, + "String06": null, + "String07": null, + "String08": null, + "String09": null, + "String10": null, + "String11": null, + "String12": null, + "AuxName": null, + "AuxType": null, + "Number01": null, + "Number02": null, + "Number03": null, + "Number04": null, + "Number05": null, + "Bool06": null, + "Bool07": null, + "Bool08": null, + "Bool09": null, + "Bool10": null, + "Date06": null, + "Date07": null, + "Bool11": null, + "Bool12": null, + "Bool13": null, + "Bool14": null, + "Bool15": null, + "Bool16": null, + "Bool17": null, + "Bool18": null, + "Bool19": null, + "Bool20": null, + "Bool21": null, + "Bool22": null, + "Bool23": null, + "Bool24": null, + "Bool25": null, + "Bool26": null, + "Bool27": null, + "Bool28": null, + "Bool29": null, + "Bool30": null, + "Bool31": null, + "Bool32": null, + "Bool33": null, + "Bool34": null, + "Bool35": null, + "Bool36": null, + "Bool37": null, + "Bool38": null, + "Bool39": null, + "Bool40": null, + "Bool41": null, + "Bool42": null, + "Bool43": null, + "Bool44": null, + "Bool45": null, + "Bool46": null, + "Bool47": null, + "Bool48": null, + "Bool49": null, + "Bool50": null, + "String13": null, + "String14": null, + "String15": null, + "String16": null, + "String17": null, + "String18": null, + "String19": null, + "String20": null, + "String21": null, + "String22": null, + "String23": null, + "String24": null, + "String25": null, + "String26": null, + "String27": null, + "String28": null, + "String29": null, + "String30": null, + "String31": null, + "String32": null, + "String33": null, + "String34": null, + "String35": null, + "String36": null, + "String37": null, + "String38": null, + "String39": null, + "String40": null, + "String41": null, + "String42": null, + "String43": null, + "String44": null, + "String45": null, + "String46": null, + "String47": null, + "String48": null, + "String49": null, + "String50": null, + "String51": null, + "String52": null, + "String53": null, + "String54": null, + "String55": null, + "String56": null, + "String57": null, + "String58": null, + "String59": null, + "String60": null, + "String61": null, + "String62": null, + "String63": null, + "String64": null, + "String65": null, + "ListOfContact": { + "Contact": { + "BirthDate": null, + "CaseRelTypeCode": null, + "ClientIDNumber": null, + "FullName": null, + "FullNameMiddle": null, + "MF": null, + "MiddleName": null, + "MiddleNameInitial": null, + "SIN": null, + "WorkPhone": null, + "ListOfContactAddresses": null + }, + "ListOfCase": { + "Case": { + "PrimaryOrganizationName": null, + "Name": null, + "Status": null, + "AssignToFN": null, + "AssignToLN": null, + "ListOfICMCaseAddress": null + } + }, + "ListOfEmployee": { + "Employee": { + "EMailAddr": null, + "Emp": null, + "Fax": null, + "FirstName": null, + "Login": null, + "FullName": null, + "FullNameMiddle": null, + "JobTitle": null, + "LastName": null, + "MiddleName": null, + "MiddleNameInitial": null, + "WorkPhoneNumber": null + } + }, + "ListOfAttCreatedBy": { + "AttCreatedBy": { + "Id": null, + "Emp": null, + "FullName": null, + "JobTitle": null + } + }, + "ListOfOffice": { + "Office": { + "OfficeRegion": null, + "OfficeAddressLine2": null, + "OfficeAddressLine3": null, + "OfficeAddressCountry": null, + "OfficeAddressUnit": null, + "OfficePhone": null, + "OfficeFax": null, + "OfficeAddressCompleteCity": null + } + } + } + }, + "versions": { + "1": { + "omitFields": [] + }, + "2": { + "omitFields": [] + } + } + }, + "HR3688E": { + "rootName": "ListOfDtFormInstanceLw", + "subRoots": ["FormInstance"], + "wrapperTags": [], + "allowCheckboxWithNoChange": [], + "omitFields": [], + "addFields": {}, + "versions": { + "1": { + "omitFields": [] + }, + "2": { + "omitFields": [] + } + } + }, + "HR3689E": { + "rootName": "ListOfDtFormInstanceLw", + "subRoots": ["FormInstance"], + "wrapperTags": [], + "allowCheckboxWithNoChange": [], + "omitFields": [], + "addFields": {}, + "versions": { + "1": { + "omitFields": [] + }, + "2": { + "omitFields": [] + } + } + }, + "HR3690E": { + "rootName": "ListOfDtFormInstanceLw", + "subRoots": ["FormInstance"], + "wrapperTags": [], + "allowCheckboxWithNoChange": [], + "omitFields": [], + "addFields": {}, + "versions": { + "1": { + "omitFields": [] + }, + "2": { + "omitFields": [] + } + } + }, + "HR3704E": { + "rootName": "ListOfDtFormInstanceLw", + "subRoots": ["FormInstance"], + "wrapperTags": [], + "allowCheckboxWithNoChange": [], + "omitFields": [], + "addFields": {}, + "versions": { + "1": { + "omitFields": [] + }, + "2": { + "omitFields": [] + } + } + } +}; + +module.exports = { formExceptions }; \ No newline at end of file diff --git a/generateHandler.js b/generateHandler.js index f764690..89d7ece 100644 --- a/generateHandler.js +++ b/generateHandler.js @@ -106,6 +106,8 @@ async function generateNewTemplate(req, res) { params = { ...params, ...configOpt }; const template_id = params["formId"]; const attachment_Id = params["attachmentId"]; + const authHeader = req.get('Authorization') || ''; + const rawToken = authHeader.split(' ')[0] === 'Bearer'? authHeader.split(' ')[1] : authHeader || null; if (!template_id) { return res .status(400) @@ -168,7 +170,16 @@ async function generateNewTemplate(req, res) { return res.status(400).send({ error: getErrorMessage("INVALID_AREA") }); } } - + + const icmOfficeName = icm_metadata?.["Office Name"]; + const requestOfficeName = typeof params.OfficeName === "string" ? params.OfficeName.trim() : ""; + console.log('RequestOfficeName:', requestOfficeName); + console.log('IcmOfficeName:', icmOfficeName); + if (!requestOfficeName && icmOfficeName) { + params.OfficeName = icmOfficeName; + } + console.log("Params:",params); + const formJson = await constructFormJson(template_id, params); if (formJson != null) { @@ -180,7 +191,7 @@ async function generateNewTemplate(req, res) { const saveDataForLater = JSON.stringify(formJson) const id = await storeData(saveDataForLater); const endPointForGenerate = process.env.GENERATE_KILN_URL + "?jsonId=" + id; - const isGenerateSuccess = await performGenerateFunction(endPointForGenerate); + const isGenerateSuccess = await performGenerateFunction(endPointForGenerate,rawToken,username); deleteData(id); //if successfully generated send back response @@ -209,7 +220,7 @@ async function generateNewTemplate(req, res) { } } -async function performGenerateFunction(url) { +async function performGenerateFunction(url,token,username) { try { const browser = await puppeteer.launch({ @@ -227,12 +238,41 @@ async function performGenerateFunction(url) { //const browser = await puppeteer.launch(); const page = await browser.newPage(); + page.on('console', msg => { + console.log('Browser logs:', msg.type(), msg.text()); + }); + + page.on('request', req => { + console.log('Request:', req.url(), req.failure()); + }); + // Set the HTML content of the page await page.goto(url,{ timeout: 200000, // 60 seconds waitUntil: 'domcontentloaded', // You can also use 'networkidle2' or 'domcontentloaded' }); // Replace with your actual URL console.log('Page loaded.'); + const appUrl = new URL(url); + const cookies = []; + + if (token) { + cookies.push({ + name: 'token', + value: token, + url: appUrl.origin + }); + } + + if (username) { + cookies.push({ + name: 'username', + value: username, + url: appUrl.origin + }); + } + if (cookies.length) { + await browser.setCookie(...cookies); + } // Step 2: Wait for the button to be available await page.waitForSelector('#generate',{ timeout: 200000 }); // Use the actual selector diff --git a/generatePDFHandler.js b/generatePDFHandler.js index cd2292f..ea0b166 100644 --- a/generatePDFHandler.js +++ b/generatePDFHandler.js @@ -3,13 +3,11 @@ const puppeteer = require('puppeteer'); const fs = require('fs'); const { validateJson } = require('./validate'); const {storeData,retrieveData,deleteData} = require('./helper/redisHelperHandler'); +const sleep = (ms) => new Promise(res => setTimeout(res, ms)); async function generatePDFFromJSON(req, res) { try { - let attachment = req.body.attachment ?? req.body[0]; - - // console.log("PDF Request:",req); - // console.log("PDF Request body:",req.body); + let attachment = req.body.attachment ?? req.body[0]; // Validate attachment is present in incoming message if (!attachment) { @@ -22,11 +20,10 @@ async function generatePDFFromJSON(req, res) { const savedJsonString = Buffer.from(attachment, 'base64').toString('utf-8'); let savedJson; - // console.log("Saved JSON String:",savedJsonString); + try { - savedJson = JSON.parse(savedJsonString); - console.log("Saved Parsed JSON:",savedJson); + savedJson = JSON.parse(savedJsonString); const { valid, errors } = validateJson(savedJson); if (valid) { @@ -60,8 +57,7 @@ async function generatePDFFromJSON(req, res) { // Generate PDF buffer const pdfBuffer = await generatePDF(savedJsonString); //get the pdf from the savedJson - const pdfBase64 = Buffer.from(pdfBuffer).toString('base64'); - // console.log("PDF Base64:",pdfBase64); + const pdfBase64 = Buffer.from(pdfBuffer).toString('base64'); res.status(200).json({ errorCode: 0, @@ -82,10 +78,9 @@ async function generatePDFFromJSON(req, res) { async function generatePDF(savedJson) { const id = await storeData(savedJson); - const endPointForPDF = process.env.GENERATE_KILN_URL + "?jsonId=" + id; - // console.log("PDF Endpoint:",endPointForPDF); - const pdfBufferFromURL = await getPDFFromURL(endPointForPDF); - deleteData(id); + const endPointForPDF = process.env.GENERATE_KILN_URL + "?jsonId=" + id; + const pdfBufferFromURL = await getPDFFromURL(endPointForPDF); + await deleteData(id); return pdfBufferFromURL; } @@ -109,6 +104,14 @@ async function generatePDFFromHTML(req, res) { ], }); const page = await browser.newPage(); + + page.on('console', msg => { + console.log('Browser logs:', msg.type(), msg.text()); + }); + + page.on('request', req => { + console.log('Request:', req.url(), req.failure()); + }); // Set the HTML content of the page await page.setContent(htmlContent, { waitUntil: 'load' }); @@ -145,8 +148,7 @@ async function generatePDFFromHTML(req, res) { async function generatePDFFromURL(req, res) { const { path } = req.body; - try { - + try { const pdfBuffer = await getPDFFromURL(path); // Send PDF back to client @@ -165,9 +167,12 @@ async function generatePDFFromURL(req, res) { } async function getPDFFromURL(url) { - - // console.log("Puppeteer path:", process.env.PUPPETEER_EXECUTABLE_PATH); - // console.log("Puppeteer URL:", url); + // Increases to 5000 seemed to be working well (could also set the params in env file if needed) + /* const HYDRATE_MS = Number(process.env.PDF_HYDRATE_MS ?? 5000); + const CLICK_WINDOW_MS = Number(process.env.PDF_CLICK_WINDOW_MS ?? 3000); + const CLICK_RETRY_EVERY_MS = Number(process.env.PDF_CLICK_INTERVAL_MS ?? 200); + const POST_CLICK_MS = Number(process.env.PDF_POST_CLICK_MS ?? 2500); */ + const browser = await puppeteer.launch({ executablePath: process.env.PUPPETEER_EXECUTABLE_PATH || "/usr/bin/chromium", args: [ @@ -183,13 +188,177 @@ async function getPDFFromURL(url) { //const browser = await puppeteer.launch(); const page = await browser.newPage(); - // Set the HTML content of the page - await page.goto(url, { waitUntil: 'networkidle2', timeout: 150000 }); + page.on('console', msg => { + console.log('Browser logs:', msg.type(), msg.text()); + }); + + page.on('request', req => { + console.log('Request:', req.url(), req.failure()); + }); // Optional: Adjust the page's viewport for better PDF layout await page.setViewport({ width: 1280, height: 800 }); + // Set the HTML content of the page + await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 150000 }); + + //The below code block clicks the show letter button on the UI. This functionality is not needed any more + // as the PRINT directly should show the letter setting/resetting visible_pdf and visbile_web flags + // in the form definition. This below code takes a lot of await times and is making the process delayed as a whole + // Until confirmed the commenting out doesnt pose any issues, leave this here just in case to put it back. + /* // Let app hydrate + await sleep(HYDRATE_MS); + + // Click “Show Letter” with a short retry window + const clickScript = ` + (function(){ + function clickByText(substr){ + substr = substr.toLowerCase(); + const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_ELEMENT); + while (walker.nextNode()){ + const el = walker.currentNode; + const tag = (el.tagName||'').toLowerCase(); + const role = el.getAttribute && el.getAttribute('role'); + const clickable = tag==='button' || tag==='a' || role==='button'; + if (!clickable) continue; + const t = (el.innerText || el.textContent || '').trim().toLowerCase(); + if (t.includes(substr)){ + el.dispatchEvent(new MouseEvent('click', {bubbles:true,cancelable:true,view:window})); + return true; + } + } + return false; + } + if (!window.__clickedShowLetter) window.__clickedShowLetter = clickByText('show letter'); + return !!window.__clickedShowLetter; + })(); + `; + + const deadline = Date.now() + CLICK_WINDOW_MS; + let clicked = await page.evaluate(clickScript); + if (!clicked) { + for (const f of page.frames()) { + try { clicked = await f.evaluate(clickScript); if (clicked) break; } catch {} + } + } + while (!clicked && Date.now() < deadline) { + await sleep(CLICK_RETRY_EVERY_MS); + clicked = await page.evaluate(clickScript); + if (!clicked) { + for (const f of page.frames()) { + try { clicked = await f.evaluate(clickScript); if (clicked) break; } catch {} + } + } + } */ + //here + + // Give it time to assemble the printable view + //await sleep(POST_CLICK_MS); + + + console.log("Waiting for print button..."); + + let clickedPrint = false; + const PRINT_DEADLINE = Date.now() + 20000; // up to 20 sec + while (!clickedPrint && Date.now() < PRINT_DEADLINE) { + // Try main document + try { + await page.waitForSelector("#print", { visible: true, timeout: 1000 }); + // Wait until enabled + await page.waitForFunction( + sel => { + const el = document.querySelector(sel); + if (!el) return false; + if (el.disabled) return false; + if (el.getAttribute("aria-disabled") === "true") return false; + if (el.classList.contains("disabled")) return false; + return true; + }, + { timeout: 5000 }, + "#print" + ); + await page.click("#print"); + clickedPrint = true; + console.log("Clicked print button on main page"); + break; + } catch {} + + // Try every frame + for (const frame of page.frames()) { + try { + await frame.waitForSelector("#print", { visible: true, timeout: 1000 }); + await frame.waitForFunction( + sel => { + const el = document.querySelector(sel); + if (!el) return false; + if (el.disabled) return false; + if (el.getAttribute("aria-disabled") === "true") return false; + if (el.classList.contains("disabled")) return false; + return true; + }, + { timeout: 5000 }, + "#print" + ); + await frame.click("#print"); + clickedPrint = true; + console.log("Clicked print button inside iframe"); + break; + } catch {} + } + + await sleep(250); // avoid hot-looping + } + + if (clickedPrint) { + + console.log("Waiting for printable layout to finish…"); + + // Wait for data-form-id attribute + try { + await page.waitForFunction( + () => document.documentElement.getAttribute("data-form-id"), + { timeout: 20000 } + ); + console.log("Printable layout ready (data-form-id detected)"); + } catch { + console.log("Timeout waiting for data-form-id"); + } + + // Svelte stabilization + console.log("Waiting for Svelte DOM stability…"); + await page.waitForFunction(() => { + const now = performance.now(); + if (!window.__lastMutationTime) window.__lastMutationTime = now; + + if (!window.__mutationObserverInstalled) { + const observer = new MutationObserver(() => { + window.__lastMutationTime = performance.now(); + }); + observer.observe(document.body, { + childList: true, + subtree: true, + attributes: true, + characterData: true + }); + window.__mutationObserverInstalled = true; + } + + return performance.now() - window.__lastMutationTime > 600; + }, { timeout: 20000 }); + console.log("Svelte DOM stabilized"); + } else { + console.log("Could not click on print button"); + } + + await page.evaluate((t) => { + document.title = t; + const titleTag = document.querySelector("title") || + document.head.appendChild(document.createElement("title")); + titleTag.textContent = t; + }, "Untitled"); + // Generate PDF with print CSS applied + await page.emulateMediaType('print'); const pdfBuffer = await page.pdf({ format: 'A4', printBackground: true, @@ -200,6 +369,7 @@ async function getPDFFromURL(url) { await browser.close(); + console.log("Returning PDF"); return pdfBuffer; } catch (error) { @@ -241,4 +411,4 @@ async function loadSavedJson(req, res) { -module.exports = { generatePDFFromHTML, generatePDFFromURL, generatePDFFromJSON, loadSavedJson }; \ No newline at end of file +module.exports = { generatePDFFromHTML, generatePDFFromURL, generatePDFFromJSON, loadSavedJson ,generatePDF }; \ No newline at end of file diff --git a/helm/templates/hpa.yaml b/helm/templates/hpa.yaml new file mode 100644 index 0000000..0c7d7c1 --- /dev/null +++ b/helm/templates/hpa.yaml @@ -0,0 +1,26 @@ +{{- if contains "prod" .Release.Namespace }} +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: communication-layer-hpa +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: communication-layer + minReplicas: 1 + maxReplicas: 3 + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: 80 + - type: Resource + resource: + name: memory + target: + type: Utilization + averageUtilization: 80 +{{- end }} \ No newline at end of file diff --git a/helm/values.yaml b/helm/values.yaml index 56ee505..b26c76b 100644 --- a/helm/values.yaml +++ b/helm/values.yaml @@ -1,26 +1,26 @@ -replicaCount: 2 - -image: - repository: ghcr.io/bcgov/communication-layer - tag: latest - pullPolicy: Always - -service: - type: ClusterIP - port: 3030 - -configMapName: commlayer - -resources: - requests: - memory: "128Mi" - cpu: "50m" - limits: - memory: "512Mi" - cpu: "250m" - -nodeSelector: {} - -tolerations: [] - -affinity: {} +replicaCount: 1 + +image: + repository: ghcr.io/bcgov/communication-layer + tag: latest + pullPolicy: Always + +service: + type: ClusterIP + port: 3030 + +configMapName: commlayer + +resources: + requests: + memory: "256Mi" + cpu: "20m" + limits: + memory: "512Mi" + cpu: "250m" + +nodeSelector: {} + +tolerations: [] + +affinity: {} diff --git a/interface.js b/interface.js new file mode 100644 index 0000000..e8350b9 --- /dev/null +++ b/interface.js @@ -0,0 +1,46 @@ +const appConfig = require('./appConfig.js'); +const { interfaces } = require("./dictionary/interfaces.js"); +async function interface(req, res) { + try { + const rawHost = (req.get("X-Original-Server") || req.hostname); + const configOpt = appConfig[rawHost]; + + // If no interface configured, return early + if (!configOpt || !configOpt.interface) { + return res.status(200).json({ + host: rawHost, + interfaceKey: null, + interface: null, + note: "No interface configured for this host" + }); + } + + const interfaceKey = configOpt.interface; + const interfaceTemplate = interfaces[interfaceKey]; + + if (!interfaceTemplate) { + return res.status(404).json({ + error: "INTERFACE_NOT_FOUND", + message: `Interface "${interfaceKey}" not found in interfaces.js.` + }); + } + + const interfaceJson = JSON.parse(JSON.stringify(interfaceTemplate)); + + return res.json({ + host: rawHost, + interfaceKey: interfaceKey, + interface: interfaceJson + }); + } + + catch (err) { + console.error("interfaceRoute error:", err); + return res.status(500).json({ + error: "INTERNAL_ERROR", + message: "Failed to build interface for this host" + }); + } +} + +module.exports = interface; \ No newline at end of file diff --git a/portal/authHandler.js b/portal/authHandler.js new file mode 100644 index 0000000..76a03a7 --- /dev/null +++ b/portal/authHandler.js @@ -0,0 +1,19 @@ +//Build Header Auth based on Portal config +function buildPortalAuthHeader(portal) { + if ( + portal?.portalAuth === "basic" && + ( + (portal.basicAuth && portal.basicAuth.username && portal.basicAuth.password) || + portal.apiSecret + ) + ) { + const creds = (portal.basicAuth && portal.basicAuth.username && portal.basicAuth.password) + ? `${portal.basicAuth.username}:${portal.basicAuth.password}` + : portal.apiSecret; // allow "user:pass" as apiSecret + const auth = "Basic " + Buffer.from(creds, "utf8").toString("base64"); + return { Authorization: auth }; + } + return {}; +} + +module.exports = buildPortalAuthHeader; diff --git a/portal/cancelPortalFormDataHandler.js b/portal/cancelPortalFormDataHandler.js new file mode 100644 index 0000000..89fe2f7 --- /dev/null +++ b/portal/cancelPortalFormDataHandler.js @@ -0,0 +1,82 @@ +// portal/expirePortalToken.js +const appConfig = require('../appConfig.js'); +const { getErrorMessage } = require("../errorHandling/errorHandler.js"); +const buildPortalAuthHeader = require('./authHandler.js'); + +async function cancelPortalAction(req, res) { + const { tokenId} = req.body || {}; + + console.log('expireNETPortal:', { + tokenId: req.body?.tokenId + }); + + try { + if (!tokenId) { + return res + .status(400) + .send({ error: getErrorMessage("FORM_NOT_FOUND_IN_REQUEST") || "Missing tokenId" }); + } + + // Resolve portal config the same way as submit + 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; + } + }) || + {}; + + const portalHost = portalConfig.apiHost; + let endpoint = req.body?.path || (portalConfig.expireTokenEndPoint || process.env.PORTAL_EXPIRE_TOKEN_ENDPOINT); + const interfaceMethod = req.body?.type || "POST"; + + const url = portalHost+endpoint; + + if (!portalHost || !endpoint) { + return res + .status(400) + .send({ error: getErrorMessage("ERROR_IN_EXECUTING_ACTION") || "Missing portalHost or endpoint path" }); + } + + const savedJson = { token: tokenId }; + + console.log('CancelForPortalAction ->', { + url, + interfaceMethod, + savedJson + }); + + const response = await fetch(url, { + method: interfaceMethod, + headers: { + "Content-Type": "application/json", + ...buildPortalAuthHeader(portalConfig), + ...(req.body?.headers || {}), + }, + body: JSON.stringify(savedJson), + }); + + console.log("Fetch status:", response.status); + + if (!response.ok) { + const text = await response.text(); + return res + .status(400) + .send({ error: getErrorMessage("ERROR_IN_EXECUTING_ACTION") || `Endpoint error: ${response.status} ${text}` }); + } + + return res.json({ status: 'success', expired: true }); + + } catch (err) { + console.error('expireNETPortal error:', err); + return res + .status(500) + .send({ error: getErrorMessage("ERROR_IN_EXECUTING_ACTION") || (err?.message || 'Unhandled error') }); + } +} + +module.exports = cancelPortalAction; diff --git a/portal/loadPortalDataHandler.js b/portal/loadPortalDataHandler.js index 0c8eabf..0864aea 100644 --- a/portal/loadPortalDataHandler.js +++ b/portal/loadPortalDataHandler.js @@ -1,6 +1,8 @@ const axios = require("axios"); const { getErrorMessage } = require("../errorHandling/errorHandler.js"); -async function getParametersFromPortal(portal,token, userId) { +const buildPortalAuthHeader = require('./authHandler.js'); + +async function getParametersFromPortal(portal,token) { let parametersForForm = ""; try { @@ -8,12 +10,12 @@ async function getParametersFromPortal(portal,token, userId) { console.log("urlForValidateTokenAndGetParams >>",urlForValidateTokenAndGetParams); const response = await axios.post(`${urlForValidateTokenAndGetParams}`, { - token, - userId + token }, { headers: { - 'Content-Type': 'application/json' + 'Content-Type': 'application/json', + ...buildPortalAuthHeader(portal), } } ); @@ -28,54 +30,22 @@ async function getParametersFromPortal(portal,token, userId) { } -async function expireTokenInPortal(portal,token, userId) { - //call another api from portal to get the params - - let isTokenExpired = false; - - try { - const urlForExpiringToken = portal.apiHost + (portal.expireTokenEndPoint || process.env.PORTAL_EXPIRE_TOKEN_ENDPOINT);; - console.log("urlForExpiringToken",urlForExpiringToken); - const response = await axios.post(`${urlForExpiringToken}`, - { - token, - userId - }, { - headers: { - 'Content-Type': 'application/json' - } - }); - - isTokenExpired = response.data; - } catch (err) { - console.log( 'Failed to contact target app', err.message ); - - return true; - } - return isTokenExpired; -} - -async function getSavedFormFromPortal(portal,token, userId) { +async function getSavedFormFromPortal(portal,token) { let parametersForForm = ""; try { const urlForValidateTokenAndGetJson= portal.apiHost+ (portal.getSavedJsonEndpoint || process.env.PORTAL_VALIDATE_TOKEN_ENDPOINT); console.log("urlForValidateTokenAndGetJson >>",urlForValidateTokenAndGetJson); - const response = await axios.post(`${urlForValidateTokenAndGetJson}`, - { - token, - userId - }, - { - headers: { - 'Content-Type': 'application/json' - } - } - ); + + const headers = { + "Content-Type": "application/json", + ...buildPortalAuthHeader(portal), + }; + const response = await axios.post(urlForValidateTokenAndGetJson, { token}, { headers }); + //TODO: verify the json against schema or some other sanity checks return response.data; - - + } catch (err) { console.log( 'Failed to contact target app', err ); @@ -84,4 +54,4 @@ async function getSavedFormFromPortal(portal,token, userId) { } -module.exports = {getParametersFromPortal , expireTokenInPortal, getSavedFormFromPortal }; \ No newline at end of file +module.exports = {getParametersFromPortal , getSavedFormFromPortal }; \ No newline at end of file diff --git a/portal/loadPortalIntegratedHandler.js b/portal/loadPortalIntegratedHandler.js index 0b7c2b1..78e616c 100644 --- a/portal/loadPortalIntegratedHandler.js +++ b/portal/loadPortalIntegratedHandler.js @@ -9,7 +9,6 @@ async function loadPortalIntegratedForm(req, res) { try { const params = req.body; const token = params["id"]; - const userId = "test"; if (!token ) { return res .status(400) @@ -22,13 +21,14 @@ async function loadPortalIntegratedForm(req, res) { if(!targetApp) { return res.status(400).send({ error: getErrorMessage("UNKNOWN_ORIGIN_SERVER") }); } - const formJson = await getSavedFormFromPortal(targetApp,token,userId); + const formJson = await getSavedFormFromPortal(targetApp,token); if(!formJson) { return res.status(400).send({ error: getErrorMessage("FORM_NOT_FOUND", { templateId: template_id }) }); } //the formJson is a base64 string . Converting to json here. - const savedJson = Buffer.from(formJson["form"], 'base64').toString('utf-8'); + console.log("Form Json:",formJson); + const savedJson = Buffer.from(formJson["formJson"], 'base64').toString('utf-8'); const data = JSON.parse(savedJson); res.status(200).send(data); diff --git a/portal/savePortalFormDataHandler.js b/portal/savePortalFormDataHandler.js index 0bc06d4..f374427 100644 --- a/portal/savePortalFormDataHandler.js +++ b/portal/savePortalFormDataHandler.js @@ -1,68 +1,81 @@ -const appConfig = require('../appConfig.js'); -async function submitForPortalAction (req,res) { - 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.action_type === 'endpoint') { - await handleEndpointAction(tokenId,action, savedForm,portalConfig); - } else { - console.warn('Unexpected action type:', action.action_type); - return res - .status(400) - .send({ error: getErrorMessage("UNKNOWN_ACTION") }); - } - } - - res.json({ status: 'success' }); - } catch(error) { - return res - .status(400) - .send({ error: getErrorMessage("ERROR_IN_EXECUTING_ACTION") }); - } -} -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(); - 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 +const appConfig = require('../appConfig.js'); +const { getErrorMessage } = require("../errorHandling/errorHandler.js"); +const buildPortalAuthHeader = require('./authHandler.js'); + +async function saveForPortalAction(req, res) { + const { tokenId, savedForm } = req.body; + + console.log('saveForPortalAction:', { + tokenId: req.body?.tokenId, + savedForm: typeof req.body?.savedForm, + }); + + try { + if (!tokenId || !savedForm) { + return res + .status(400) + .send({ error: "Missing tokenId or savedForm" }); + } + + 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; + } + }) || {}; + + const portalHost = portalConfig.apiHost; + let endpoint = req.body?.path ||portalConfig.saveEndpoint; + const interfaceMethod = req.body?.type || "POST"; + + + const url = portalHost+endpoint; + + if (!portalHost || !endpoint) { + return res + .status(400) + .send({ error: getErrorMessage("ERROR_IN_EXECUTING_ACTION") || "Missing portalHost or endpoint path" }); + } + + const base64EncodedJson = Buffer.from(savedForm, "utf8").toString("base64"); + const savedJson = { token: tokenId, jsonToSave: base64EncodedJson }; + + console.log('SaveForPortalAction ->', { + url, + interfaceMethod, + savedJson + }); + + const response = await fetch(url, { + method: interfaceMethod, + headers: { + "Content-Type": "application/json", + ...buildPortalAuthHeader(portalConfig), + ...(req.body?.headers || {}), + }, + body: JSON.stringify(savedJson), + }); + + console.log("Fetch status:", response.status); + + if (!response.ok) { + const text = await response.text(); + return res + .status(400) + .send({ error: getErrorMessage("ERROR_IN_EXECUTING_ACTION") || `Endpoint error: ${response.status} ${text}` }); + } + + res.json({ status: "success" }); + } catch (error) { + console.error("saveForPortalAction error:", error); + return res + .status(400) + .send({ error: getErrorMessage("ERROR_IN_EXECUTING_ACTION") }); + } +} + +module.exports = saveForPortalAction; diff --git a/portal/submitPortalFormDataHandler.js b/portal/submitPortalFormDataHandler.js new file mode 100644 index 0000000..50b818c --- /dev/null +++ b/portal/submitPortalFormDataHandler.js @@ -0,0 +1,76 @@ +const appConfig = require('../appConfig.js'); +const { getErrorMessage } = require("../errorHandling/errorHandler.js"); +const buildPortalAuthHeader = require('./authHandler.js'); + +async function submitForPortalAction(req, res) { + const { tokenId } = req.body; + + try { + if (!tokenId) { + return res + .status(400) + .send({ error:"Missing tokenId or form" }); + } + + 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; + } + }) || + {}; + + const interfaceHost = portalConfig.apiHost; + let submitPath = req.body?.path ||portalConfig.submitEndpoint; + const interfaceMethod = req.body?.type || "POST"; + + if (!interfaceHost || !submitPath) { + return res + .status(500) + .send({ error: getErrorMessage("PORTAL_CONFIG_NOT_FOUND") || "Missing interface host/endpoints in appConfig" }); + } + + const submitUrl = interfaceHost+submitPath; + + console.log("SUBMIT URL",submitUrl); + + const headers = { + 'Content-Type': 'application/json', + ...buildPortalAuthHeader(portalConfig), + ...(req.body?.headers || {}), + }; + + const savePayload = { + token: tokenId + }; + + + const submitResp = await fetch(submitUrl.toString(), { + method: interfaceMethod, + headers, + body: JSON.stringify(savePayload) + }); + + console.log("SUBMIT RESPONSE",submitResp); + + if (!submitResp.ok) { + const text = await submitResp.text(); + return res + .status(502) + .send({ error: `Submit failed: ${submitResp.status} ${text}` }); + } + + return res.json({ status: 'success' }); + } catch (err) { + console.error('submitNETPortal error:', err); + return res + .status(500) + .send({ error: getErrorMessage("ERROR_IN_EXECUTING_ACTION") || (err?.message || 'Unhandled error') }); + } +} + +module.exports = submitForPortalAction; diff --git a/routes.js b/routes.js index 3a1a28a..6fcb57e 100644 --- a/routes.js +++ b/routes.js @@ -3,15 +3,18 @@ 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 } = require("./saveICMdataHandler"); +const { saveICMdata, loadICMdata, clearICMLockedFlag, loadICMdataAsPDF } = 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 saveForPortalAction = require("./portal/savePortalFormDataHandler.js"); const loadPortalIntegratedForm = require("./portal/loadPortalIntegratedHandler.js"); +const submitForPortalAction = require("./portal/submitPortalFormDataHandler.js"); +const cancelForPortalAction = require('./portal/cancelPortalFormDataHandler.js'); +const interface = require("./interface.js"); require("./formRepoHandler"); const {getProcessedData} = require("./icmJsonClobHandler"); const router = express.Router(); @@ -197,7 +200,11 @@ router.post("/loadSavedJson", loadSavedJson); router.post("/generatePortalForm", generatePortalIntegratedTemplate); router.post("/generateNewTemplate", generateNewTemplate); router.use('/pdfRender', renderRouter); -router.use('/submitForPortalAction', submitForPortalAction); +router.use('/saveForPortalAction', saveForPortalAction); router.post("/loadPortalForm", loadPortalIntegratedForm); +router.use("/interface", interface); +router.post('/submitForPortalAction', submitForPortalAction); +router.post('/cancelForPortalAction', cancelForPortalAction); +router.post("/loadICMDataAsPDF", loadICMdataAsPDF); module.exports = router; \ No newline at end of file diff --git a/saveICMdataHandler.js b/saveICMdataHandler.js index 9834e0e..ebc5cc9 100644 --- a/saveICMdataHandler.js +++ b/saveICMdataHandler.js @@ -7,11 +7,16 @@ const { getUsername, isUsernameValid } = require("./usernameHandler.js"); const { getErrorMessage } = require("./errorHandling/errorHandler.js"); const { isJsonStringValid } = require("./validate.js"); const appCfg = require('./appConfig.js'); +const { toICMFormat } = require("./dateConverter.js"); +const { formExceptions } = require("./dictionary/jsonXmlConversion.js"); +const { propertyExists, propertyNotEmpty, keyExists } = require("./dictionary/dictionaryUtils.js"); +const {generatePDF }= require("./generatePDFHandler.js"); const SIEBEL_ICM_API_FORMS_ENDPOINT = process.env.SIEBEL_ICM_API_FORMS_ENDPOINT; -// utility function to fetch Attachment status (In Progress, Open...) -// and Locked By User field -async function getICMAttachmentStatus(attachment_id, username, params) { + +// Fetch attachment status and locked-by fields from ICM +// authHeaders: optional pre-acquired headers to avoid duplicate Keycloak token calls +async function getICMAttachmentStatus(attachment_id, username, params, authHeaders = null) { let return_data = {}; return_data["Status"] = ""; return_data["Locked by User"] = ""; @@ -30,12 +35,18 @@ async function getICMAttachmentStatus(attachment_id, username, params) { let url = buildUrlWithParams(params["apiHost"], params["saveEndpoint"] + attachment_id + '/', params); try { let response; - const grant = - await keycloakForSiebel.grantManager.obtainFromClientCredentials(); - const headers = { - Authorization: `Bearer ${grant.id_token.token}`, - "X-ICM-TrustedUsername": username, + let headers; + + if (authHeaders) { + headers = authHeaders; + } else { + const grant = await keycloakForSiebel.grantManager.obtainFromClientCredentials(); + headers = { + Authorization: `Bearer ${grant.id_token.token}`, + "X-ICM-TrustedUsername": username, + }; } + const query = { viewMode: "Catalog" } @@ -116,7 +127,16 @@ async function saveICMdata(req, res) { .status(401) .send({ error: getErrorMessage("FORM_ALREADY_FINALIZED") }); } - //saveForm validate before saving to ICM + + //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. + if (!includesData) { + console.log('JSON is not valid '); + return res + .status(400) + .send({ error: getErrorMessage("FORM_NOT_VALID") }); + } + const valid = isJsonStringValid(savedFormParam); if (!valid) { console.log('JSON is not valid '); @@ -132,32 +152,79 @@ async function saveICMdata(req, res) { 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 - 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); + 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 : {}; + + 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])}; } - const wrapperKey = {} - wrapperKey[newKey] = childrenArray; - truncatedKeysSaveData[`${newKey}-List`] = wrapperKey // Add a wrapper around the children/dependecies + }); + } + + // 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 { - truncatedKeysSaveData[newKey] = saveData[oldKey]; //Data is added to new JSON with the truncated key + builder = new xml2js.Builder({xmldec: { version: '1.0' }, renderOpts: { pretty: false }}); } - } - let builder = new xml2js.Builder({xmldec: { version: '1.0' }}); - saveJson["XML Hierarchy"] = builder.buildObject(truncatedKeysSaveData); + + 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 + + // console.log("XML Hierarchy:", xml); + console.log("XML Hierarchy length (chars):", xml.length); + console.log("XML Hierarchy size (bytes):", xmlSize); let url = buildUrlWithParams(params["apiHost"], params["saveEndpoint"] + attachment_id + '/', params); try { let response; @@ -193,7 +260,7 @@ async function loadICMdata(req, res) { let params = req.body; const rawHost = (req.get("X-Original-Server") || req.hostname); const configOpt = appCfg[rawHost]; - params = { ...params,...configOpt }; + params = { ...params,...configOpt }; const attachment_id = params["attachmentId"]; const office_name = params["OfficeName"]; console.log("attachment_id>>", attachment_id); @@ -215,9 +282,23 @@ async function loadICMdata(req, res) { return res .status(401) .send({ error: getErrorMessage("INVALID_USER") }); - } - let icm_metadata = await getICMAttachmentStatus(attachment_id, username, params); - let icm_status = icm_metadata["Status"]; + } + + // Acquire token once and reuse for all ICM calls + let authHeaders; + try { + const grant = await keycloakForSiebel.grantManager.obtainFromClientCredentials(); + authHeaders = { + Authorization: `Bearer ${grant.id_token.token}`, + "X-ICM-TrustedUsername": username, + }; + } catch (error) { + console.error("Failed to acquire Keycloak token:", error); + return res.status(500).send({ error: getErrorMessage("GENERIC_ERROR_MSG") }); + } + + let icm_metadata = await getICMAttachmentStatus(attachment_id, username, params, authHeaders); + let icm_status = icm_metadata["Status"]; if (!icm_status || icm_status == "") { console.log("Error fetching Form Instance Thin data for ", attachment_id); return res @@ -228,12 +309,6 @@ async function loadICMdata(req, res) { let url = buildUrlWithParams(params["apiHost"], params["saveEndpoint"] + attachment_id + '/', params); try { let response; - const grant = - await keycloakForSiebel.grantManager.obtainFromClientCredentials(); - const headers = { - Authorization: `Bearer ${grant.id_token.token}`, - "X-ICM-TrustedUsername": username, - } const query = { viewMode: "Catalog", inlineattachment: true @@ -241,9 +316,9 @@ async function loadICMdata(req, res) { if (params.icmWorkspace) { query.workspace = params.icmWorkspace; } - response = await axios.get(url, { params: query, headers }); + response = await axios.get(url, { params: query, headers: authHeaders }); let return_data = Buffer.from(response.data["Doc Attachment Id"], 'base64').toString('utf-8'); - //validate the returned data to be of the expected format + //validate the returned data to be of the expected format const valid = isJsonStringValid(return_data); if (!valid) { console.log('JSON is not valid '); @@ -336,6 +411,7 @@ async function clearICMLockedFlag(req, res) { if (params.icmWorkspace) { query.workspace = params.icmWorkspace; } + response = await axios.put(url, saveJson, { params: query, headers }); return res.status(200).send({}); } @@ -346,7 +422,479 @@ async function clearICMLockedFlag(req, res) { } } + +/** 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 + * @returns checkboxItemsId : This will contain all of the IDs of the checkbox fields + * @returns textInfoFields : This will contain all of the IDs of the text-info fields + */ +function getFormIds (formDefinitionItems) { + let dateItemsId = []; + let checkboxItemsId = []; + let textInfoFields = []; + 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); + else if (subItem.type === "container") { + const {dateItemsId: recursiveDateItemIds, checkboxItemsId: recursiveCheckboxItemIds, textInfoFields: recursiveTextInfoFields} = getFormIds([subItem]); + dateItemsId = [...dateItemsId, ...recursiveDateItemIds]; + checkboxItemsId = [...checkboxItemsId, ...recursiveCheckboxItemIds]; + textInfoFields = [ ...textInfoFields, ...recursiveTextInfoFields]; + } + }); + } 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); + }); + }); + } 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); + } + }); + return { dateItemsId, checkboxItemsId, textInfoFields }; +} + +/** + * Convert the checkbox value to one of the following: + * true -> "Yes" + * false -> "No" + * undefined -> "" + */ +function convertCheckboxFormatToICM (value) { + if (value === true) return "Yes"; + else if (value === false) return "No"; + else return ""; +} + +/** Recursive function that gets all of the wrapper tags from a value in a dictionary's wrapperTags + * + * @param keyPair : {} + * @param topLevelKeys : [] + * @returns + */ +function getWrapperIds (keyPair, topLevelKeys) { + const keys = Object.keys(keyPair); + if (typeof keyPair === "undefined" || keys.length === 0) return undefined + let toWrapIds = {}; + keys.forEach(key => { + if (typeof keyPair[key] === "number") { + toWrapIds[key] = {tags: topLevelKeys, level: keyPair[key]}; + } else if (typeof keyPair[key] === "object") { + const tempKeyWrappers = topLevelKeys.concat([key]); + const tempWrapperContainer = getWrapperIds(keyPair[key], tempKeyWrappers); + if (typeof tempWrapperContainer != undefined) { + Object.keys(tempWrapperContainer).forEach(value => { + toWrapIds[value] = tempWrapperContainer[value]; + }); + } + } + }); + return toWrapIds; +} + +/** When multiple wrappers are listed in dictionary for a uuid, wrap for JSON -> XML here + * + * @param truncatedKeysSaveData + * @param toWrapIds + * @param dataToWrap + * @param oldKey + * @param newKey + * @param isChild : boolean, checks if the multilevel wrapper was called from the child uuid shortening if-else + * @returns + */ +function multilevelWrappers(truncatedKeysSaveData, toWrapIds, dataToWrap, oldKey, newKey, isChild){ + if (toWrapIds[oldKey].tags.length === 1 || toWrapIds[oldKey].level === 0) { //One wrapper + if(isChild) { + const wrapperKey = {}; + wrapperKey[newKey] = dataToWrap; + truncatedKeysSaveData[toWrapIds[oldKey].tags[0]][`${newKey}-List`] = wrapperKey; // Add a wrapper around the children/dependecies + } else { + truncatedKeysSaveData[toWrapIds[oldKey].tags[0]][newKey] = dataToWrap; + } + } else if (toWrapIds[oldKey].tags.length === 2 || toWrapIds[oldKey].level === 1) { //Two wrappers + if (!truncatedKeysSaveData[toWrapIds[oldKey].tags[0]][toWrapIds[oldKey].tags[1]]) { + truncatedKeysSaveData[toWrapIds[oldKey].tags[0]][toWrapIds[oldKey].tags[1]] = {}; + } + if(isChild) { + const wrapperKey = {}; + wrapperKey[newKey] = dataToWrap; + truncatedKeysSaveData[toWrapIds[oldKey].tags[0]][toWrapIds[oldKey].tags[1]][`${newKey}-List`] = wrapperKey; + } else { + truncatedKeysSaveData[toWrapIds[oldKey].tags[0]][toWrapIds[oldKey].tags[1]][newKey] = dataToWrap; + } + } else if (toWrapIds[oldKey].tags.length === 3 || toWrapIds[oldKey].level === 2) { //Three wrappers + if (!truncatedKeysSaveData[toWrapIds[oldKey].tags[0]][toWrapIds[oldKey].tags[1]]) { + truncatedKeysSaveData[toWrapIds[oldKey].tags[0]][toWrapIds[oldKey].tags[1]] = {}; + } + if (!truncatedKeysSaveData[toWrapIds[oldKey].tags[0]][toWrapIds[oldKey].tags[1]][toWrapIds[oldKey].tags[2]]){ + truncatedKeysSaveData[toWrapIds[oldKey].tags[0]][toWrapIds[oldKey].tags[1]][toWrapIds[oldKey].tags[2]] = {}; + } + if(isChild) { + const wrapperKey = {}; + wrapperKey[newKey] = dataToWrap; + truncatedKeysSaveData[toWrapIds[oldKey].tags[0]][toWrapIds[oldKey].tags[1]][toWrapIds[oldKey].tags[2]][`${newKey}-List`] = wrapperKey; + } else { + truncatedKeysSaveData[toWrapIds[oldKey].tags[0]][toWrapIds[oldKey].tags[1]][toWrapIds[oldKey].tags[2]][newKey] = dataToWrap; + } + } else if (toWrapIds[oldKey].tags.length === 4 || toWrapIds[oldKey].level === 3) { //Four wrappers + if (!truncatedKeysSaveData[toWrapIds[oldKey].tags[0]][toWrapIds[oldKey].tags[1]]) { + truncatedKeysSaveData[toWrapIds[oldKey].tags[0]][toWrapIds[oldKey].tags[1]] = {}; + } + if (!truncatedKeysSaveData[toWrapIds[oldKey].tags[0]][toWrapIds[oldKey].tags[1]][toWrapIds[oldKey].tags[2]]){ + truncatedKeysSaveData[toWrapIds[oldKey].tags[0]][toWrapIds[oldKey].tags[1]][toWrapIds[oldKey].tags[2]] = {}; + } + if (!truncatedKeysSaveData[toWrapIds[oldKey].tags[0]][toWrapIds[oldKey].tags[1]][toWrapIds[oldKey].tags[2]][toWrapIds[oldKey].tags[3]]){ + truncatedKeysSaveData[toWrapIds[oldKey].tags[0]][toWrapIds[oldKey].tags[1]][toWrapIds[oldKey].tags[2]][toWrapIds[oldKey].tags[3]] = {}; + } + if(isChild) { + const wrapperKey = {}; + wrapperKey[newKey] = dataToWrap; + truncatedKeysSaveData[toWrapIds[oldKey].tags[0]][toWrapIds[oldKey].tags[1]][toWrapIds[oldKey].tags[2]][toWrapIds[oldKey].tags[3]][`${newKey}-List`] = wrapperKey; + } else { + truncatedKeysSaveData[toWrapIds[oldKey].tags[0]][toWrapIds[oldKey].tags[1]][toWrapIds[oldKey].tags[2]][toWrapIds[oldKey].tags[3]][newKey] = dataToWrap; + } + } + return truncatedKeysSaveData +} + +/** Take the saveData JSON values and update them to new layout. Changes include: + * Shorten UUID. + * Wrap values with children inside a List. + * Wrap specified values under a parent. + * Change date format. + * Convert checkboxes to use "Yes" or "No". + * + * @param saveData : = JSON.parse(savedFormParam)["data"] + * @param truncatedKeysSaveData : list of key-object pairs that will be returned at end of function. These may or may not already include values related to toWrapIds. + * @param toWrapIds : list of key-string pairs where the keys are UUIDs and the strings are what wrapper tag they need to go under. + * @param dateItemsId : list of date fields + * @param checkboxItemsId : list of checkbox fields + * @param textInfoFields : list of text-info fields. These will be omitted from adding to the XML + * @param noCheckboxChange : list of checkbox UUIDs that should not have their value changed + * @param omitFields : list of field UUIDS that should not be included in the XML + * @param addFields : list of empty fields to add for form to succeed in saving as XML + * @param {number} kilnVersion : the Kiln version matching the save data json layout + * @returns truncatedKeysSaveData : a list of key-object pairs + */ + +function fixJSONValuesForXML (saveData, truncatedKeysSaveData, toWrapIds, dateItemsId, checkboxItemsId, textInfoFields, noCheckboxChange, omitFields, addFields, kilnVersion) { + for(let oldKey in saveData) { //This begins trunicating the JSON keys for XML (UUID should be first 8 characters) + if (!omitFields.includes(oldKey) && !textInfoFields.includes(oldKey)) { + 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]) { + if (kilnVersion === 1) { // Kiln V1 + const childStringLength = oldChildKey.length; + const newChildKey = oldChildKey.substring(stringLength+3, childStringLength-28); + if (!omitFields.includes(oldChildKey.substring(stringLength+3, childStringLength)) && !textInfoFields.includes(oldChildKey.substring(stringLength+3, childStringLength))) { + if (dateItemsId.includes(oldChildKey.substring(stringLength+3, childStringLength))) { // If child data is in a date field, change date format from YYYY-MM-DD to MM/DD/YYYY + const newDateFormat = toICMFormat(saveData[oldKey][i][oldChildKey]); + if (newDateFormat === "-1") { + throw new Error("Invalid date. Was unable to convert to ICM format!"); + } + truncatedChildrenKeys[newChildKey] = newDateFormat; + } else if (checkboxItemsId.includes(oldChildKey.substring(stringLength+3, childStringLength)) && !noCheckboxChange.includes(oldChildKey.substring(stringLength+3, childStringLength))) { // If child data is in a checkbox field AND is not listed for ommission, change from true/false/undefined to Yes/No/"" + truncatedChildrenKeys[newChildKey] = convertCheckboxFormatToICM(saveData[oldKey][i][oldChildKey]); + } else { + truncatedChildrenKeys[newChildKey] = saveData[oldKey][i][oldChildKey]; + } + } + } + else { // Kiln V2 + const childStringLength = oldChildKey.length; + const newChildKey = oldChildKey.substring(0, childStringLength-28); + if (!omitFields.includes(oldChildKey.substring(0, childStringLength)) && !textInfoFields.includes(oldChildKey.substring(0, childStringLength))) { + if (dateItemsId.includes(oldChildKey.substring(0, childStringLength))) { // If child data is in a date field, change date format from YYYY-MM-DD to MM/DD/YYYY + const newDateFormat = toICMFormat(saveData[oldKey][i][oldChildKey]); + if (newDateFormat === "-1") { + throw new Error("Invalid date. Was unable to convert to ICM format!"); + } + truncatedChildrenKeys[newChildKey] = newDateFormat; + } else if (checkboxItemsId.includes(oldChildKey.substring(0, childStringLength)) && !noCheckboxChange.includes(oldChildKey.substring(0, childStringLength))) { // If child data is in a checkbox field AND is not listed for ommission, change from true/false/undefined to Yes/No/"" + truncatedChildrenKeys[newChildKey] = convertCheckboxFormatToICM(saveData[oldKey][i][oldChildKey]); + } else { + truncatedChildrenKeys[newChildKey] = saveData[oldKey][i][oldChildKey]; + } + } + } + } + if (Object.keys(truncatedChildrenKeys).length != 0) { + childrenArray.push(truncatedChildrenKeys); + } + } + if (toWrapIds[oldKey]) { + if (!truncatedKeysSaveData[toWrapIds[oldKey].tags[0]]) { //Initalize wrapper if top level doesn't exist + truncatedKeysSaveData[toWrapIds[oldKey].tags[0]] = {}; + } + truncatedKeysSaveData = multilevelWrappers(truncatedKeysSaveData, toWrapIds, childrenArray, oldKey, newKey, true); + } else { + const wrapperKey = {}; + wrapperKey[newKey] = childrenArray; + truncatedKeysSaveData[`${newKey}-List`] = wrapperKey; // Add a wrapper around the children/dependecies + } + } else { + if (dateItemsId.includes(oldKey)) { // If data is in a date field, change date format from YYYY-MM-DD to MM/DD/YYYY + const newDateFormat = toICMFormat(saveData[oldKey]); + if (newDateFormat === "-1") { + throw new Error("Invalid date. Was unable to convert to ICM format!"); + } + if (toWrapIds[oldKey]) { + if (!truncatedKeysSaveData[toWrapIds[oldKey].tags[0]]) { //Initalize wrapper if top level doesn't exist + truncatedKeysSaveData[toWrapIds[oldKey].tags[0]] = {}; + } + truncatedKeysSaveData = multilevelWrappers(truncatedKeysSaveData, toWrapIds, newDateFormat, oldKey, newKey, false); + } else { + truncatedKeysSaveData[newKey] = newDateFormat; + } + } else if (checkboxItemsId.includes(oldKey) && !noCheckboxChange.includes(oldKey)) { // If data is in a checkbox field AND is not listed for ommission, change from true/false/undefined to Yes/No/"" + if (toWrapIds[oldKey]) { + if (!truncatedKeysSaveData[toWrapIds[oldKey].tags[0]]) { //Initalize wrapper if top level doesn't exist + truncatedKeysSaveData[toWrapIds[oldKey].tags[0]] = {}; + } + const newCheckboxFormat = convertCheckboxFormatToICM(saveData[oldKey]); + truncatedKeysSaveData = multilevelWrappers(truncatedKeysSaveData, toWrapIds, newCheckboxFormat, oldKey, newKey, false); + } else { + truncatedKeysSaveData[newKey] = convertCheckboxFormatToICM(saveData[oldKey]); + } + } else { + if (toWrapIds[oldKey]) { + if (!truncatedKeysSaveData[toWrapIds[oldKey].tags[0]]) { //Initalize wrapper if top level doesn't exist + truncatedKeysSaveData[toWrapIds[oldKey].tags[0]] = {}; + } + truncatedKeysSaveData = multilevelWrappers(truncatedKeysSaveData, toWrapIds, saveData[oldKey], oldKey, newKey, false); + } else { + truncatedKeysSaveData[newKey] = saveData[oldKey]; //Data is added to new JSON with the truncated key + } + } + } + } + } + //Check additional fields needed before returning final value + const additionalFieldKeys = Object.keys(addFields); + if (additionalFieldKeys.length > 0) { + additionalFieldKeys.forEach(key => { + if (addFields[key] === null) { //Parent key/field. If it passes, then this should not exist in form already because this is for ADDING fields. If it exists in form, it will be overwritten. + truncatedKeysSaveData[key] = null; + } else if (truncatedKeysSaveData[key] === undefined) { //If truncatedKeysSaveData[key] does not exist, create it with all values under the add key from dictionary + truncatedKeysSaveData[key] = addFields[key]; + } else { //Has children in dictionary AND truncatedKeysSaveData exists already + + //Consider making the following a recursive function in a future iteration + /** + * For each key: + * Check to see if field is undefined. If undefined, then define it as empty + * Get the keys of the object if avaliable. Otherwise, get empty array + * If truncatedKeysSaveData keys lead to a group/list: apply values from dictionary + * Else if there are child keys avaliable: check the child keys (i.e. repeat this for each key) + * Else: save current level truncatedKeysSaveData with the current dictionary key values + */ + + const childFields1 = addFields[key] != null ? Object.keys(addFields[key]) : []; + if (truncatedKeysSaveData[key] && truncatedKeysSaveData[key].length != undefined) { + truncatedKeysSaveData[key][childKey1].forEach((value, index) => { + truncatedKeysSaveData[key][index] = {...truncatedKeysSaveData[key][index], ...addFields[key]}; + }); + } else if(childFields1.length > 0) { + childFields1.forEach(childKey1 => { + if (truncatedKeysSaveData[key][childKey1] === undefined) { + truncatedKeysSaveData[key][childKey1] = {}; + } + const childFields2 = addFields[key][childKey1] != null ? Object.keys(addFields[key][childKey1]) : []; + if (truncatedKeysSaveData[key][childKey1] && truncatedKeysSaveData[key][childKey1].length != undefined) { //Add the second layer of empty fields to groups + truncatedKeysSaveData[key][childKey1].forEach((value, index) => { + truncatedKeysSaveData[key][childKey1][index] = {...truncatedKeysSaveData[key][childKey1][index], ...addFields[key][childKey1]}; + }); + } else if (childFields2.length > 0) { //Continue if there are more fields listed + childFields2.forEach(childKey2 => { + if (truncatedKeysSaveData[key][childKey1][childKey2] === undefined) { + truncatedKeysSaveData[key][childKey1][childKey2] = {}; + } + const childFields3 = addFields[key][childKey1][childKey2] != null ? Object.keys(addFields[key][childKey1][childKey2]) : []; + if (truncatedKeysSaveData[key][childKey1][childKey2] && truncatedKeysSaveData[key][childKey1][childKey2].length != undefined) { + truncatedKeysSaveData[key][childKey1][childKey2].forEach((value, index) => { + truncatedKeysSaveData[key][childKey1][childKey2][index] = {...truncatedKeysSaveData[key][childKey1][childKey2][index], ...addFields[key][childKey1][childKey2]}; + }); + } else if (childFields3.length > 0) { + childFields3.forEach(childKey3 => { + if (truncatedKeysSaveData[key][childKey1][childKey2][childKey3] === undefined) { + truncatedKeysSaveData[key][childKey1][childKey2][childKey3] = {}; + } + const childFields4 = addFields[key][childKey1][childKey2][childKey3] != null ? Object.keys(addFields[key][childKey1][childKey2][childKey3]) : []; + if (truncatedKeysSaveData[key][childKey1][childKey2][childKey3] && truncatedKeysSaveData[key][childKey1][childKey2][childKey3].length != undefined) { + truncatedKeysSaveData[key][childKey1][childKey2][childKey3].forEach((value, index) => { + truncatedKeysSaveData[key][childKey1][childKey2][childKey3][index] = {...truncatedKeysSaveData[key][childKey1][childKey2][childKey3][index], ...addFields[key][childKey1][childKey2][childKey3]}; + }); + } else if (childFields4.length > 0) { + childFields4.forEach(childKey4 => { + if (truncatedKeysSaveData[key][childKey1][childKey2][childKey3][childKey4] === undefined) { + truncatedKeysSaveData[key][childKey1][childKey2][childKey3][childKey4] = {}; + } + const childFields5 = addFields[key][childKey1][childKey2][childKey3][childKey4] != null ? Object.keys(addFields[key][childKey1][childKey2][childKey3][childKey4]) : []; + if (truncatedKeysSaveData[key][childKey1][childKey2][childKey3][childKey4] && truncatedKeysSaveData[key][childKey1][childKey2][childKey3][childKey4].length != undefined) { + truncatedKeysSaveData[key][childKey1][childKey2][childKey3][childKey4].forEach((value, index) => { + truncatedKeysSaveData[key][childKey1][childKey2][childKey3][childKey4][index] = {...truncatedKeysSaveData[key][childKey1][childKey2][childKey3][childKey4][index], ...addFields[key][childKey1][childKey2][childKey3][childKey4]}; + }); + } else if (childFields5.length > 0) { + childFields5.forEach(childKey5 => { + if (truncatedKeysSaveData[key][childKey1][childKey2][childKey3][childKey4][childKey5] === undefined) { + truncatedKeysSaveData[key][childKey1][childKey2][childKey3][childKey4][childKey5] = {}; + } + const childFields6 = addFields[key][childKey1][childKey2][childKey3][childKey4][childKey5] != null ? Object.keys(addFields[key][childKey1][childKey2][childKey3][childKey4][childKey5]) : []; + if (truncatedKeysSaveData[key][childKey1][childKey2][childKey3][childKey4][childKey5] && truncatedKeysSaveData[key][childKey1][childKey2][childKey3][childKey4][childKey5].length != undefined) { + truncatedKeysSaveData[key][childKey1][childKey2][childKey3][childKey4][childKey5].forEach((value, index) => { + truncatedKeysSaveData[key][childKey1][childKey2][childKey3][childKey4][childKey5][index] = {...truncatedKeysSaveData[key][childKey1][childKey2][childKey3][childKey4][childKey5][index], ...addFields[key][childKey1][childKey2][childKey3][childKey4][childKey5]}; + }); + } else if (childFields6.length > 0) { + childFields6.forEach(childKey6 => { + if (truncatedKeysSaveData[key][childKey1][childKey2][childKey3][childKey4][childKey5][childKey6] === undefined) { + truncatedKeysSaveData[key][childKey1][childKey2][childKey3][childKey4][childKey5][childKey6] = {}; + } + if (truncatedKeysSaveData[key][childKey1][childKey2][childKey3][childKey4][childKey5][childKey6] && truncatedKeysSaveData[key][childKey1][childKey2][childKey3][childKey4][childKey5][childKey6].length != undefined) { + truncatedKeysSaveData[key][childKey1][childKey2][childKey3][childKey4][childKey5].forEach((value, index) => { + truncatedKeysSaveData[key][childKey1][childKey2][childKey3][childKey4][childKey5][childKey6][index] = {...truncatedKeysSaveData[key][childKey1][childKey2][childKey3][childKey4][childKey5][childKey6][index], ...addFields[key][childKey1][childKey2][childKey3][childKey4][childKey5][childKey6]}; + }); + } else { + truncatedKeysSaveData[key][childKey1][childKey2][childKey3][childKey4][childKey5][childKey6] = addFields[key][childKey1][childKey2][childKey3][childKey4][childKey5][childKey6]; + } + }); + } else { + truncatedKeysSaveData[key][childKey1][childKey2][childKey3][childKey4][childKey5] = addFields[key][childKey1][childKey2][childKey3][childKey4][childKey5]; + } + }); + } else { + truncatedKeysSaveData[key][childKey1][childKey2][childKey3][childKey4] = addFields[key][childKey1][childKey2][childKey3][childKey4]; + } + }); + } else { + truncatedKeysSaveData[key][childKey1][childKey2][childKey3] = addFields[key][childKey1][childKey2][childKey3]; + } + }); + } else { + truncatedKeysSaveData[key][childKey1][childKey2] = addFields[key][childKey1][childKey2]; + } + }); + } else { //If value is null or if truncatedKeysSaveData[key][childKey1] did not exist + truncatedKeysSaveData[key][childKey1] = addFields[key][childKey1]; + } + }); + } else { //The parent key has not been made yet, so make it and add dictionary values. This is just a precaution because the scenario should be solved during addFields null check + truncatedKeysSaveData[key] = addFields[key]; + } + } + }); + } + return truncatedKeysSaveData; +} + +async function loadICMdataAsPDF(req,res) { + + //load the savedJson form ICM. + let params = req.body; + const rawHost = (req.get("X-Original-Server") || req.hostname); + const configOpt = appCfg[rawHost]; + params = { ...params,...configOpt }; + const attachment_id = params["attachmentId"]; + const office_name = params["OfficeName"]; + console.log("attachment_id>>", attachment_id); + if (!attachment_id) { + return res + .status(400) + .send({ error: getErrorMessage("ATTACHMENT_ID_REQUIRED") }); + } + let username = null; + + if (params["token"]) { + username = await getUsername(params["token"], params["employeeEndpoint"]); + } else if (params["username"]) { + const valid = await isUsernameValid(params["username"], params["employeeEndpoint"]); + username = valid ? params["username"] : null; + } + + if (!username || !isNaN(username)) { + return res + .status(401) + .send({ error: getErrorMessage("INVALID_USER") }); + } + + // Acquire token once and reuse for all ICM calls + let authHeaders; + try { + const grant = await keycloakForSiebel.grantManager.obtainFromClientCredentials(); + authHeaders = { + Authorization: `Bearer ${grant.id_token.token}`, + "X-ICM-TrustedUsername": username, + }; + } catch (error) { + console.error("Failed to acquire Keycloak token:", error); + return res.status(500).send({ error: getErrorMessage("GENERIC_ERROR_MSG") }); + } + + let icm_metadata = await getICMAttachmentStatus(attachment_id, username, params, authHeaders); + let icm_status = icm_metadata["Status"]; + if (!icm_status || icm_status == "") { + console.log("Error fetching Form Instance Thin data for ", attachment_id); + return res + .status(400) + .send({ error: getErrorMessage("FORM_STATUS_NOT_FOUND") }); + } + //let url = buildUrlWithParams('SIEBEL_ICM_API_HOST', 'fwd/v1.0/data/DT FormFoundry Upsert/DT Form Instance Orbeon Revise/'+attachment_id+'/',''); + let url = buildUrlWithParams(params["apiHost"], params["saveEndpoint"] + attachment_id + '/', params); + try { + let response; + const query = { + viewMode: "Catalog", + inlineattachment: true + } + if (params.icmWorkspace) { + query.workspace = params.icmWorkspace; + } + response = await axios.get(url, { params: query, headers: authHeaders }); + let return_data = Buffer.from(response.data["Doc Attachment Id"], 'base64').toString('utf-8'); + //validate the returned data to be of the expected format + const valid = isJsonStringValid(return_data); + if (!valid) { + console.log('JSON is not valid '); + return res + .status(400) + .send({ error: getErrorMessage("FORM_NOT_VALID") }); + } + //validate ends here. Once validated continue to modify json based on status and send back. + if (icm_status == "Complete") { + let new_data = JSON.parse(return_data); + new_data.form_definition.readOnly = true; + return_data = JSON.stringify(new_data); + } + + const pdfBuffer = await generatePDF(return_data); //get the pdf from the savedJson + + const pdfBase64 = Buffer.from(pdfBuffer).toString('base64'); + res.status(200).json({ + pdf_base64: pdfBase64 + }); + } + catch (error) { + console.error(`Error loading data from ICM for PDF:`, error); + return res + .status(400) + .send({ error: getErrorMessage("GENERIC_ERROR_MSG") }); + } +} + module.exports.saveICMdata = saveICMdata; module.exports.loadICMdata = loadICMdata; module.exports.clearICMLockedFlag = clearICMLockedFlag; -module.exports.getICMAttachmentStatus = getICMAttachmentStatus; \ No newline at end of file +module.exports.getICMAttachmentStatus = getICMAttachmentStatus; +module.exports.loadICMdataAsPDF = loadICMdataAsPDF; \ No newline at end of file diff --git a/schema/form_definitionV2.yaml b/schema/form_definitionV2.yaml new file mode 100644 index 0000000..c5a1ee1 --- /dev/null +++ b/schema/form_definitionV2.yaml @@ -0,0 +1,327 @@ +type: object +required: + - id + - version + - ministry_id + - data +properties: + version: + anyOf: + - type: string + - type: number + ministry_id: + anyOf: + - type: string + - type: number + id: + type: string + lastModified: + type: string + title: + type: string + readOnly: + type: boolean + form_id: + type: string + deployed_to: + anyOf: + - type: string + - type: "null" + dataSources: + type: array + items: + type: object + default: [] + data: + type: array + form_data: + type: object + properties: + form_id: + anyOf: + - type: number + - type: string + form_developer: + type: object + comments: + anyOf: + - type: string + - type: "null" + created_at: + type: string + updated_at: + type: string + interface: + type: array + styles: + type: array + scripts: + type: array + elements: + type: array + items: + type: object + properties: + fields: + type: array + items: + $ref: "#/definitions/Item" + +definitions: + Item: + required: + - uuid + - type + type: object + properties: + type: + type: string + name: + type: string + description: + anyOf: + - type: string + - type: "null" + help_text: + anyOf: + - type: string + - type: "null" + is_required: + type: boolean + visible_web: + type: boolean + visible_pdf: + type: boolean + custom_visibility: + anyOf: + - type: string + - type: "null" + is_read_only: + type: boolean + save_on_submit: + type: boolean + order: + type: number + options: + type: array + parent_id: + anyOf: + - type: number + - type: "null" + attributes: + type: array + items: + type: object + properties: + hideLabel: + type: boolean + defaultChecked: + type: boolean + deletedAt: + anyOf: + - type: string + - type: "null" + text: + type: string + kind: + type: string + placeholder: + type: string + minDate: + type: string + maxDate: + type: string + dateFormat: + type: string + formatStyle: + type: string + max: + type: string + min: + type: string + step: + type: string + value: + type: string + label: + anyOf: + - type: string + - type: "null" + placeholder: + anyOf: + - type: string + - type: "null" + id: + type: string + mask: + anyOf: + - type: string + - type: "null" + codeContext: + type: object + properties: + name: + type: string + header: + type: string + offText: + type: string + onText: + type: string + size: + type: string + listItems: + type: array + items: + type: object + properties: + value: + type: string + text: + type: string + required: + - text + - value + groupItems: + type: array + items: + type: object + properties: + fields: + type: array + items: + $ref: "#/definitions/Item" + repeater: + type: boolean + helperText: + anyOf: + - type: string + - type: "null" + value: + anyOf: + - type: string + - type: "null" + filenameStatus: + type: string + labelDescription: + type: string + initialRows: + type: string + initialColumns: + type: string + initialHeaderNames: + type: string + repeaterItemLabel: + type: string + validation: + type: array + items: + type: object + properties: + type: + type: string + enum: + - required + - pattern + - minLength + - maxLength + - minDate + - maxDate + - minValue + - maxValue + - javascript + value: + anyOf: + - type: string + - type: number + - type: boolean + errorMessage: + anyOf: + - type: string + - type: "null" + required: + - type + - value + - errorMessage + conditions: + type: array + items: + type: object + properties: + type: + type: string + enum: + - visibility + - readOnly + - calculatedValue + - saveOnSubmit + value: + type: string + required: + - type + - value + additionalProperties: false + webStyles: + anyOf: + - type: "null" + - type: object + additionalProperties: + anyOf: + - type: string + - type: number + - type: boolean + - type: object + - type: array + - type: "null" + pdfStyles: + anyOf: + - type: "null" + - type: object + additionalProperties: + anyOf: + - type: string + - type: number + - type: boolean + - type: object + - type: array + - type: "null" + databindings: + 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: + $ref: "#/definitions/Item" diff --git a/validate.js b/validate.js index 00c9373..4c66e10 100644 --- a/validate.js +++ b/validate.js @@ -11,8 +11,17 @@ function loadSchema(schemaPath) { // Function to validate JSON data against a YAML schema function validateJson(jsonData) { + + /** + * Apply Kiln Version + * Kiln V1 uses data: { items: []} + * Kiln V2 uses dataSources [] + */ + const kilnVersion = Object.keys(jsonData["data"]).includes("items") ? 1 + : Object.keys(jsonData?.["form_definition"]["data"]).includes("items") ? 1 + : 2; const schema = loadSchema("schema/saved_json.yaml"); - const formDefinitionSchema = loadSchema("schema/form_definition.yaml"); + const formDefinitionSchema = kilnVersion === 1 ? loadSchema("schema/form_definition.yaml") : loadSchema("schema/form_definitionV2.yaml"); const ajv = new Ajv({ allErrors: true }); addFormats(ajv);