Tuesday 24 September 2024

Fixing the 'Failed to Save Prompt' Error in Google Cloud Vertex AI Lab - Invalid structured dataset metadata value

 

For two days, I encountered the "Invalid structured dataset metadata value" error while attempting to save a prompt in the Google Cloud Vertex AI lab (see the screenshot below). Each time, I provided feedback with comments, screenshots, and a rating to express my frustration.



Though I’m unsure if they acted on my feedback, after two days, the issue was resolved. The key takeaway: Ensure the region specified for saving matches what is mentioned in the course. This small detail can save you a lot of trouble!

Wednesday 24 July 2024

Dynamic Data Collection and Nested Structure Creation in LWC with Apex

To handle various types of input elements dynamically, you can use a more generic approach to select all form elements and collect their values. You'll need to modify the JavaScript code to handle different types of inputs (e.g., radio buttons, checkboxes, dropdowns) accordingly.

LWC JavaScript Level

HTML Template:
html
Copy code
<template>

    <lightning-input label="Name" name="EditableFields.Name"></lightning-input>

    <lightning-input label="Age" name="EditableFields.Age" type="number"></lightning-input>

    <lightning-input label="Mom" name="EditableFields.Parents.Mom"></lightning-input>

    <lightning-input label="Dad" name="EditableFields.Parents.Dad"></lightning-input>

    <lightning-radio-group 

        label="Gender" 

        name="EditableFields.Gender"

        options={genderOptions}

        onchange={handleChange}>

    </lightning-radio-group>

    <lightning-combobox

        name="EditableFields.Country"

        label="Country"

        placeholder="Select Country"

        options={countryOptions}

        onchange={handleChange}>

    </lightning-combobox>

    <lightning-button label="Submit" onclick={handleSubmit}></lightning-button>

</template>


JavaScript Controller:
javascript
Copy code
import { LightningElement } from 'lwc';

import submitData from '@salesforce/apex/YourApexClass.submitData';


export default class EditableForm extends LightningElement {

    genderOptions = [

        { label: 'Male', value: 'Male' },

        { label: 'Female', value: 'Female' },

        { label: 'Other', value: 'Other' }

    ];


    countryOptions = [

        { label: 'USA', value: 'USA' },

        { label: 'Canada', value: 'Canada' },

        { label: 'India', value: 'India' }

    ];


    handleSubmit() {

        const fields = {};

        const inputs = this.template.querySelectorAll('lightning-input, lightning-radio-group, lightning-combobox');


        inputs.forEach(input => {

            if (input.type === 'checkbox') {

                fields[input.name] = input.checked;

            } else if (input.type === 'radio') {

                fields[input.name] = input.value;

            } else {

                fields[input.name] = input.value;

            }

        });


        submitData({ fields: JSON.stringify(fields) })

            .then(result => {

                console.log('Submitted Data: ', result);

            })

            .catch(error => {

                console.error('Error: ', error);

            });

    }

}


Apex Controller

Apex Controller:
apex
Copy code
public with sharing class YourApexClass {

    @AuraEnabled

    public static String submitData(String fields) {

        try {

            Map<String, Object> fieldsMap = (Map<String, Object>) JSON.deserializeUntyped(fields);


            Map<String, Object> customerData = new Map<String, Object>();

            customerData.put('CustomerId', 'D1');

            customerData.put('IdempotencyKey', '012ADQ');

            Map<String, Object> editableFields = new Map<String, Object>();

            customerData.put('EditableFields', editableFields);


            for (String path : fieldsMap.keySet()) {

                dynamicInsert(editableFields, path.split('\\.'), fieldsMap.get(path));

            }


            return JSON.serialize(customerData);

        } catch (Exception e) {

            throw new AuraHandledException('Error: ' + e.getMessage());

        }

    }


    private static void dynamicInsert(Map<String, Object> map, List<String> pathParts, Object value) {

        if (pathParts.size() == 1) {

            map.put(pathParts[0], value);

            return;

        }


        String key = pathParts[0];

        if (!map.containsKey(key)) {

            map.put(key, new Map<String, Object>());

        }


        dynamicInsert((Map<String, Object>) map.get(key), pathParts.subList(1, pathParts.size()), value);

    }

}


Explanation

  1. LWC JavaScript Level:

    • The handleSubmit method is triggered when the submit button is clicked.

    • It selects all lightning-input, lightning-radio-group, and lightning-combobox elements and iterates over them to build the fields object, where the key is the name attribute (which contains the bind path) and the value is the input's value.

    • For checkbox inputs, it stores the checked state; for radio buttons and other inputs, it stores the value.

    • The fields object is then serialized to JSON and sent to the Apex controller using the submitData method.

  2. Apex Controller:

    • The submitData method receives the JSON string and deserializes it into a Map<String, Object>.

    • customerData is initialized with fixed values for CustomerId and IdempotencyKey.

    • For each entry in the fieldsMap, the dynamicInsert method is called to construct the nested structure.

    • dynamicInsert is a recursive method that navigates through the path parts and dynamically creates nested maps as needed, inserting the value at the correct position.

This approach ensures that you can collect all input data with a single handler and dynamically create the required structure in Apex based on the received bind fields, regardless of the type of input element.

Sunday 21 July 2024

Embedding External Content in Salesforce using Apex Callouts and Lightning Web Components

1. Can achieve this using Apex by creating a callout to the external service from Salesforce and then displaying the response within an iframe. Here's a detailed approach to accomplish this without using a proxy server:

Step 1: Create an Apex Controller for the Callout

Create an Apex class that performs the callout to the external service, includes the API key in the headers, and returns the response.

apex

public class ExternalServiceController { @AuraEnabled(cacheable=true) public static String getExternalContent() { String apiKey = 'YOUR_API_KEY'; Http http = new Http(); HttpRequest request = new HttpRequest(); request.setEndpoint('https://external.url'); request.setMethod('GET'); request.setHeader('Authorization', 'Bearer ' + apiKey); HttpResponse response = http.send(request); if (response.getStatusCode() == 200) { return response.getBody(); } else { throw new AuraHandledException('Error: ' + response.getStatus()); } } }

Step 2: Create a Lightning Web Component (LWC)

Create a Lightning Web Component that calls the Apex method and displays the response within an iframe or div.

LWC JavaScript Controller

javascript

import { LightningElement, track, wire } from 'lwc'; import getExternalContent from '@salesforce/apex/ExternalServiceController.getExternalContent'; export default class ExternalContentComponent extends LightningElement { @track externalContent; @wire(getExternalContent) wiredContent({ error, data }) { if (data) { this.externalContent = data; } else if (error) { this.externalContent = 'Error fetching content: ' + error.body.message; } } }

LWC HTML Template

html

<template> <template if:true={externalContent}> <div class="external-content" lwc:dom="manual"></div> </template> <template if:true={error}> <div class="error-message">{error}</div> </template> </template>

LWC CSS (Optional)

css

.external-content { width: 100%; height: 600px; overflow: auto; }

Step 3: Render External Content

Since you cannot directly render HTML within an iframe from the response body, you'll need to set the content dynamically using JavaScript.

LWC JavaScript Controller (Updated)

javascript

import { LightningElement, track, wire } from 'lwc'; import getExternalContent from '@salesforce/apex/ExternalServiceController.getExternalContent'; export default class ExternalContentComponent extends LightningElement { @track externalContent; @wire(getExternalContent) wiredContent({ error, data }) { if (data) { this.externalContent = data; this.renderExternalContent(); } else if (error) { this.externalContent = 'Error fetching content: ' + error.body.message; } } renderExternalContent() { if (this.externalContent) { const contentDiv = this.template.querySelector('.external-content'); if (contentDiv) { contentDiv.innerHTML = this.externalContent; } } } }

Security Considerations

  • CSP (Content Security Policy): Ensure that your Salesforce instance allows loading content from the external service.
  • API Key Security: Ensure the API key is handled securely and not exposed in client-side code.
  • Sanitization: Make sure to sanitize the response if it contains HTML to avoid XSS (Cross-Site Scripting) attacks.

This approach leverages Apex to handle the API call and set the necessary headers, then dynamically injects the content into your Lightning component. This avoids the need for a proxy server and leverages Salesforce's capabilities directly.


2.

Let's consider an alternative approach using Visualforce and a custom Visualforce controller to handle the callout and display the response within an iframe.

Using Visualforce with Apex

Step 1: Create an Apex Controller

Create an Apex class to handle the callout.

apex
public class ExternalServiceController { public String getExternalContent() { String apiKey = 'YOUR_API_KEY'; Http http = new Http(); HttpRequest request = new HttpRequest(); request.setEndpoint('https://external.url'); request.setMethod('GET'); request.setHeader('Authorization', 'Bearer ' + apiKey); HttpResponse response = http.send(request); if (response.getStatusCode() == 200) { return response.getBody(); } else { throw new VisualforceException('Error: ' + response.getStatus()); } } }

Step 2: Create a Visualforce Page

Create a Visualforce page to embed the external content.

html
<apex:page controller="ExternalServiceController" contentType="text/html"> <apex:form> <apex:outputPanel layout="block"> <apex:iframe src="data:text/html;base64,{!BASE64ENCODE(getExternalContent())}" width="100%" height="600px"/> </apex:outputPanel> </apex:form> </apex:page>

Explanation

  1. Apex Controller:

    • The ExternalServiceController class makes a callout to the external service, includes the API key in the headers, and returns the response body.
  2. Visualforce Page:

    • The Visualforce page uses the ExternalServiceController to fetch the external content.
    • The apex:iframe tag embeds the content within an iframe, converting the response body to a base64-encoded string to safely embed it.

Security Considerations

  • CSP (Content Security Policy): Ensure that your Salesforce instance allows the external URL.
  • API Key Security: Securely handle the API key.
  • Sanitization: Ensure the response does not contain any malicious content.

By using Visualforce, you can effectively embed external content with headers set in the Apex callout. This approach provides a viable solution within the Salesforce framework, leveraging Visualforce's capability to embed content securely.

3.

Using Apex Callout and Custom Object

Step 1: Apex Controller for Callout

apex

public class ExternalServiceController { @AuraEnabled public static String getExternalContent() { String apiKey = 'YOUR_API_KEY'; Http http = new Http(); HttpRequest request = new HttpRequest(); request.setEndpoint('https://external.url'); request.setMethod('GET'); request.setHeader('Authorization', 'Bearer ' + apiKey); HttpResponse response = http.send(request); if (response.getStatusCode() == 200) { return response.getBody(); } else { throw new AuraHandledException('Error: ' + response.getStatus()); } } }

Step 2: LWC to Display Content

  1. LWC JavaScript Controller

    javascript

    import { LightningElement, track, wire } from 'lwc'; import getExternalContent from '@salesforce/apex/ExternalServiceController.getExternalContent'; export default class ExternalContentComponent extends LightningElement { @track externalContent; @wire(getExternalContent) wiredContent({ error, data }) { if (data) { this.externalContent = data; } else if (error) { this.externalContent = 'Error fetching content: ' + error.body.message; } } }
  2. LWC HTML Template

    html

    <template> <template if:true={externalContent}> <div lwc:dom="manual" class="external-content"></div> </template> <template if:true={error}> <div class="error-message">{error}</div> </template> </template>
  3. LWC JavaScript to Inject Content

    javascript

    export default class ExternalContentComponent extends LightningElement { @track externalContent; @wire(getExternalContent) wiredContent({ error, data }) { if (data) { this.externalContent = data; this.renderExternalContent(); } else if (error) { this.externalContent = 'Error fetching content: ' + error.body.message; } } renderExternalContent() { if (this.externalContent) { const contentDiv = this.template.querySelector('.external-content'); if (contentDiv) { contentDiv.innerHTML = this.externalContent; } } } }

Considerations:

  • CSP (Content Security Policy): Ensure that your Salesforce CSP settings allow the external URL.
  • Sanitization: Make sure to sanitize any HTML content to avoid XSS attacks.
  • Security: Ensure the API key is securely stored and not exposed.
3.Calling an external system's URL from within Salesforce, and the external system needs to authenticate the request by recognizing that you're a valid client. You want to do this by passing the API key in the HTTP header rather than in the query string, and then displaying the content within Salesforce (likely in an iframe or similar).

Approach: Passing API Key in Header for External System Authentication

Step 1: Create an Apex Controller to Perform the HTTP Callout

  • Create an Apex class that sends an HTTP request to the external system's URL with the API key in the HTTP header.
  • The external system will authenticate the request based on the API key provided in the header and return the content or a signed URL.

Step 2: Return the Signed URL or Content

  • If the external system returns a signed URL (or some other link that includes authentication), return this URL from the Apex class.
  • Alternatively, if the system returns the actual HTML content, return this directly.

Step 3: Create LWC to Render the Content

  • In the LWC: If the Apex method returns a signed URL, set this as the src of an iframe. If HTML content is returned, render it directly.

Example Implementation

Apex Controller (ExternalSystemController.apex):


public class ExternalSystemController { @AuraEnabled(cacheable=true) public static String fetchSignedUrlOrContent() { HttpRequest req = new HttpRequest(); HttpResponse res; Http http = new Http(); req.setMethod('GET'); req.setEndpoint('https://external-system-url.com/resource'); // Replace with the actual URL req.setHeader('Authorization', 'Bearer YOUR_API_KEY'); // Pass the API key in the header try { res = http.send(req); if (res.getStatusCode() == 200) { return res.getBody(); // Assuming this is the signed URL or content } else { throw new CalloutException('Request failed with status code: ' + res.getStatusCode()); } } catch (Exception e) { throw new CalloutException('Error during HTTP callout: ' + e.getMessage()); } } }

LWC to Display the Content:

LWC Template (externalContent.html):
html
<template> <lightning-card title="External System Content"> <iframe src={iframeSrc} width="100%" height="600px" if:true={iframeSrc}></iframe> <div lwc:dom="manual" if:true={htmlContent}></div> </lightning-card> </template>
LWC JavaScript (externalContent.js):
javascript
import { LightningElement, track, wire } from 'lwc'; import fetchSignedUrlOrContent from '@salesforce/apex/ExternalSystemController.fetchSignedUrlOrContent'; export default class ExternalContent extends LightningElement { @track iframeSrc; @track htmlContent; @wire(fetchSignedUrlOrContent) wiredUrlOrContent({ error, data }) { if (data) { if (data.startsWith('http')) { this.iframeSrc = data; // If it's a URL, set it as iframe src } else { this.htmlContent = data; // Otherwise, treat it as HTML content } } else if (error) { console.error('Error fetching data from external system', error); } } }

Step 4: Deploy and Test

  • Deploy the Apex class and LWC to your Salesforce org.
  • Add the LWC to a Lightning page to verify that the content from the external system loads correctly, with the API key securely passed in the HTTP header.

Summary

  • Apex handles the HTTP callout to the external system, passing the API key in the header.
  • LWC dynamically displays the returned signed URL in an iframe or the returned HTML content directly.

This approach ensures that the external system authenticates your request without exposing the API key in the query string.


4.an alternative way to achieve the same goal, where you can authenticate with the external system without directly exposing or handling the API key in Salesforce. Here are two alternate methods:

Alternative 1: Use a Middleware or Proxy Server

Overview

  • Implement a middleware or proxy server that handles the API key and authentication. Salesforce would make requests to this middleware, which then forwards the request to the external system with the API key in the header.
  • This approach abstracts the authentication process from Salesforce, keeping the API key secure and maintaining a clean separation between systems.

Steps

  1. Set Up a Middleware Server:

    • Create a simple middleware server using a technology like Node.js, Express, or any preferred web framework.
    • This server should accept requests from Salesforce, attach the API key in the header, and forward the request to the external system.
  2. Salesforce to Middleware:

    • In Salesforce, make an HTTP request to the middleware server from an Apex class.
    • The middleware adds the necessary headers (including the API key) and forwards the request to the external system.
  3. Middleware to External System:

    • The middleware receives the response from the external system and sends it back to Salesforce.
    • The response can be a signed URL or the actual content.
  4. Render in Salesforce:

    • Use LWC to render the content or URL returned from the middleware.

Advantages:

  • Keeps API key secure and hidden from Salesforce.
  • Simplifies Salesforce implementation as it only needs to communicate with the middleware.

Alternative 2: OAuth 2.0 Authentication

Overview

  • If the external system supports OAuth 2.0, you can set up OAuth authentication between Salesforce and the external system.
  • Salesforce will obtain an access token to authenticate API calls instead of directly passing an API key.

Steps

  1. OAuth Setup:

    • Register Salesforce as a client in the external system's OAuth 2.0 setup.
    • Obtain client credentials (client ID and secret).
  2. Implement OAuth Flow in Salesforce:

    • Use an Apex class to handle the OAuth 2.0 authentication flow (e.g., client credentials flow).
    • Salesforce will send a request to the external system’s token endpoint to obtain an access token.
  3. Use Access Token:

    • Once the access token is obtained, use it in the header of subsequent API requests to the external system.
    • The external system authenticates based on the access token, ensuring secure access.
  4. Render in Salesforce:

    • After receiving the response from the external system (signed URL or content), render it in Salesforce using LWC.

Advantages:

  • OAuth 2.0 is a secure and standardized authentication method.
  • No need to handle or store API keys directly in Salesforce.

Summary

  • Middleware Approach: Salesforce communicates with a proxy server that handles the API key and forwards the request.
  • OAuth 2.0: Use OAuth to securely authenticate and obtain access tokens, avoiding direct API key usage.

Both alternatives offer more secure and scalable solutions, depending on your infrastructure and the external system's capabilities.

Friday 3 May 2024

Limitations on Accessing Local Files in Salesforce Apex

Have you ever found yourself wrestling with Salesforce Apex, trying to figure out how to directly access files stored on your local drive? If you're like me, you might have spent hours trying to make it work, only to hit a brick wall.

Let me save you the headache – it's not possible. And trust me, I've banged my head against this particular limitation all morning. Why? Because Salesforce operates in a highly secure environment, and for good reason. While Apex is incredibly powerful, it's not designed to breach the security barriers that protect user data.

You cannot directly read a file from a user's local drive using Salesforce Apex by providing a file path. Salesforce runs in a highly secure, multi-tenant environment, and for security reasons, it doesn't have direct access to a user's local file system.

However, if the file is already uploaded into Salesforce (e.g., as an attachment, ContentDocument, or a custom object storing file data), you can certainly manipulate and read that file using Apex.

If the file is stored externally (e.g., on a server accessible via HTTP), you can use Apex to make HTTP callouts to retrieve the file's content.

But directly accessing files on a user's local drive is not possible due to Salesforce's security model.