How to Use Namespace in Web Component

Using namesapce in your org, which is must if you developing managed package, can cause some issues. Especially, when using objects/fields with their API names inside lightning web component (similar issues exist also in Visualforce pages...).


Consider the following example: I have org with namespace appsolu_utils and custom object Log_Message__c, therefore the object full name is: appsolu_utils__Log_Message__c

The object have custom fields: 

appsolu_utils__Subject__c 

appsolu_utils__Log_Type__c 


Now, writing simple apex code that retrieve the object records and return it as list:

public with sharing class ViewLogMessageController {    
    @AuraEnabled
    public static List<Log_Message__c> getAllLogs(){
        
        
        return [    SELECT Id, Subject__c,Log_Type__c
                    FROM Log_Message__c];
    }
}


As you can see we don't have to use the namespace in apex code (although we can)


Using this function in web component, and using the object fields in web component html file:

import { LightningElement, track } from 'lwc';
import getAllLogs from '@salesforce/apex/ViewLogMessageController.getAllLogs';

export default class ViewLogMessages extends LightningElement {

    @track logMessages;

    connectedCallback(){
        if(! this.logMessages){
            getAllLogs().then(
                result => {
                    console.log('result:: ' + JSON.stringify(result));
                    this.logMessages = result;
                }
            );
        }
    }
}

<template>
    <template if:true={logMessages}>
        <table>
            <thead>
                <tr>
                    <th>Type</th>
                    <th>Subject</th>
                </tr>
            </thead>
            <tbody>
                <template for:each={logMessages} for:item="msg">
                    <tr key={msg.Id}>
                        <td>{msg.Log_Type__c}</td>
                        <td>{msg.Subject__c}</td>
                    </tr>
                </template>
            </tbody>
        </table>
    </template>
</template>

However if I will load the component it will show table with empty values, and by looking at console log the issue is clear- the object/fields does have namespace, therefore the fields with values are: appsolu_utils__Subject__c,  appsolu_utils__Log_Type__c




In fact, if you will debug the server code you will notice that the namespace exists also there, only in the server Salesforce does some "magic" so you don't have to write it. The "magic" unfortunately doesn't happen also in the javascript/html code.


So how can we solve it?

Of course, it can be fixed by using the object + fields with there namespace in the web component code, but this will probably hit you back later, if you will create new dev org without namespace or different namespace and you will have to change all the places were the namespace is hardcode. Therefore, hardcoding the namespace should be the last option.


Other solutions?

1.Using wrapper class instead of the object/fields API names

Very common solution is to use wrapper class. I can modify the controller code to:

public with sharing class ViewLogMessageController {    

    @AuraEnabled
    public static List<LogMessage> getAllLogs(){
        List<LogMessage> logList = new List<LogMessage>();

        for(Log_Message__c log : [  SELECT Id, Subject__c,Log_Type__c
                                    FROM Log_Message__c]){
            logList.add(new LogMessage(log));
        }
        
        return logList;
    }

    public class LogMessage{
        @AuraEnabled
        public String Subject;
        @AuraEnabled
        public String Log_Type;

        public LogMessage(Log_Message__c log){
            Subject = log.Subject__c;
            Log_Type = log.Log_Type__c;
        }
    }
}


In the html file, just change the iteration reference to:

<template for:each={logMessages} for:item="msg">
    <tr key={msg.Id}>
        <td>{msg.Log_Type}</td>
        <td>{msg.Subject}</td>
    </tr>
</template>


And it will work. 

Disadvantages of this approach:

-Might be lots of code, as we will need such wrapper for each custom object

-Require maintenance when adding/modifying fields.


2.Stripe the namespace in the server

We can think of each record as a map of field=>value (which is actually the case), and convert the SObject record to a real Map. Only the key that will be used for the map, will be without namespace.

See the updated code, where I'm removing the namespace in the server with a code that can be reused for any object type:

public with sharing class ViewLogMessageController {    
    @AuraEnabled
    public static List<Map<String, object>> getAllLogs(){
        
        ApexClass cls = [SELECT NamespacePrefix FROM ApexClass WHERE Name = 'ViewLogMessageController'];
        String namespace = String.isBlank(cls.NamespacePrefix) ? '' : cls.NamespacePrefix + '__';

        List<Log_Message__c> logList = [    SELECT Id, Subject__c,Log_Type__c
                                            FROM Log_Message__c];
        
        return getListAsMap(logList, namespace);
    }


    //Get object as map. This is needed as in lwc the name getting with namespace, therefore need to convert it to map before sending response
    public static Map<String, object> getObjectAsMap(sObject rec, string namespace){
        Map<String, object> mapData = new Map<String, object>();

        map<String, Schema.SObjectField> fieldMap = Schema.getGlobalDescribe().get(rec.getSObjectType().getDescribe().getName()).getDescribe().fields.getMap();

        for(String field : rec.getPopulatedFieldsAsMap().keyset()) {
            //If it is not reference get the field value
            //if it is reference then either lookup with field or list of records
            if(! field.endsWith('__r')){
                mapData.put(field.replace(namespace, ''), rec.get(field));
            }
            else if(fieldMap.containsKey(field.replace('__r', '__c'))){
                mapData.put(field.replace(namespace, ''), getObjectAsMap(rec.getSObject(field), namespace));
            }
            else{
                mapData.put(field.replace(namespace, ''), getListAsMap(rec.getSObjects(field), namespace));
            }
        }
        
        return mapData;
    }

    public static List<Map<String, object>> getListAsMap(List<sObject> recList, String namespace){
        List<Map<String, object>> listData = new List<Map<String, object>>();

        for(sObject rec : recList){
            listData.add(getObjectAsMap(rec, namespace));
        }

        return listData;
    }
}


At the beginning of the method getAllLogs I'm using query to find current org namespce, it is probably good idea to store it in metadata/label and save this query.

After that I'm calling method getListAsMap which convert the list of records to list of maps. 


3.Remove the namespace in the javascript code

Similar to the second approach, only here we will remove the namespace in the javascript code.

In such solution the apex code, remain as the original code

public with sharing class ViewLogMessageController {    
    @AuraEnabled
    public static List<Log_Message__c> getAllLogs(){
        
        
        return [    SELECT Id, Subject__c,Log_Type__c
                    FROM Log_Message__c];
    }
}

But in the javascript will implement few changes. First, I'm storing the namespace in custom label and import it. Later, I 'm using it to remove the namespace from each field in the result

import { LightningElement, track } from 'lwc';
import namespaceLabel from '@salesforce/label/c.Namespace_Prefix';
import getAllLogs from '@salesforce/apex/ViewLogMessageController.getAllLogs';

export default class ViewLogMessages extends LightningElement {

    @track logMessages;

    label = {
        namespaceLabel
    }

    connectedCallback(){
        if(! this.logMessages){
            getAllLogs().then(
                result => {
                    let namespace = this.label.namespaceLabel == 'na' ? '' : this.label.namespaceLabel;
                    this.logMessages = [];

                    for(let log in result){
                        let logItem = {};

                        for(let field in result[log]){
                            logItem[field.substring(field.indexOf(namespace) + namespace.length)] = result[log][field];
                        }

                        this.logMessages.push(logItem);
                    }
                }
            );
        }
    }
}


Note that in both method the conversion should happen also in the other direction - if the web component send the data back to the server to be saved then the wrapper/map should be converted back to custom object.


Also notice that you might get namespace issues in other areas in your web component. For example, using Navigate to object home tab, won't work if it is missing the namespace

this[NavigationMixin.Navigate]({
    type: 'standard__objectPage',
    attributes:{
        objectApiName: 'Log_Message__c',
        actionName: 'home'
    }
});

To solve this case, you can store the namespace either in metadata/label, retrieve it in the javascript and use it whenever you referencing the object name

this[NavigationMixin.Navigate]({
    type: 'standard__objectPage',
    attributes:{
        objectApiName: (namespace + 'Log_Message__c'),
        actionName: 'home'
    }
});


No comments:

Post a Comment

Retire of Permission on Profiles

If you are working as a Salesforce admin/developer you've probably heard somewhere that Salesforce is planning to make a significant cha...