Showing posts with label SFDC. Show all posts
Showing posts with label SFDC. Show all posts

Saturday, 22 August 2020

App Builder Page Aware Lightning Web Component

Introduction

This week I've been experimenting with decoupled Lightning Web Components in app builder pages, now that we have the Lightning Message Service to allow them to communicate with each other without being nested. 

A number of the components are intended for use in record home and app pages, which can present challenges, in my case around initialisation. Some of my components need to initialise by making a callout to the server, but if they are part of a record home page then I want to wait until the record id has been supplied. Ideally I want my component to know what type of page it is currently being displayed in and take appropriate action.

Inspecting the URL

One way to achieve this is to inspect the URL of the current page, but this is a pretty brittle approach as if Salesforce change the URL scheme then it stands a good chance of breaking. I could use the navigation mixin to generate URLs from page references and compare those with the current URL, but that seems a little clunky and adds a delay while I wait for the promises to resolve.

targetConfig Properties

The solution I found with the least impact was to use targetConfig stanzas in the component's js-meta.xml configuration file. From the docs, these allow you to:

Configure the component for different page types and define component properties. For example, a component could have different properties on a record home page than on the Salesforce Home page or on an app page.

It was this paragraph that gave me the clue - different properties depending on the page type!

You can define the same property across multiple page types, but define different default values depending on the specific page type. In my case, I define a pageType property and default to the type of page that I am targeting:

<targetConfigs>
    <targetConfig targets="lightning__RecordPage">
        <property label="pageType" name="pageType" type="String" default="record" required="true"/>
    </targetConfig>
    <targetConfig targets="lightning__AppPage">
        <property label="pageType" name="pageType" type="String" default="app" required="true"/>
    </targetConfig>
    <targetConfig targets="lightning__HomePage">
        <property label="pageType" name="pageType" type="String" default="home" required="true"/>
    </targetConfig>
</targetConfigs>

so for a record page, the pageType property is set as 'record' and so on.

In my component, I expose page type as a public property with getter and setter methods (you only need the @api decorator on one of the methods, and convention right now seems to be the getter) :

@api get pageType() {
    return this._pageType;
}

set pageType(value) {
    this._pageType=value;
    this.details+='I am in a(n) ' + this._pageType + ' page\n';
    switch (this._pageType) {
        case 'record' :
             this.details+='Initialisation will happen when the record id  is set (which may already have happened)\n';
             break;
            ;;
        case 'app' :
        case 'home' :
            this.details+='Initialising\n';
            break;
    }                
 }

and similar for the record id, so that I can take some action when that is set:

@api get recordId() {
    return this._recordId;
}

set recordId(value) {
    this._recordId=value;
    this.details+='I have received record id ' + this._recordId + ' - initialising\n';
}

then I can add the component to the record page for Accounts:



a custom app page:



and the home page for the sales standard application:

and in each case the component knows what type of page it has been added to. 

Of course this isn't foolproof - my Evil Co-Worker could edit the pages and change the values in the app builder, leading to all manner of hilarity as my components wait forlornly for the record id that never comes. I could probably extend my Org Documentor to process the flexipage metadata and check the values haven't been changed, but in reality this is fairly low impact sabotage and probably better that the Evil Co-Worker focuses on this rather than something more damaging.

Show Me The Code!

You can find the code at the Github repo.

Related Posts


Saturday, 15 August 2020

Org Documentor and AuraEnabled Classes


Introduction

In the Winter 21 release of Salesforce, access to AuraEnabled methods in Apex classes will be restricted to users with profiles or permission sets that grant access to those classes. If you've been working in a sandbox recently you've probably encountered this already, as the critical update for this was enabled on August 8th.

Figuring out who (if anyone) has access to classes isn't straightforward, as you have to trawl through the profiles and permission sets and check each one. Salesforce have created an Aura Enabled Scanner application that can be installed via an unlocked package, which checks packaged and unpackaged code, but it does require that you login to Salesforce each time you need to check things.

This seemed like a good candidate for my Org Documentor Salesforce CLI Plug-In - something that can be run on a schedule, check that any new classes used as controllers for Aura or Lightning Web Components are accessible, and show which profiles/permission sets have access.


Configuration

To add processing for AuraEnabled classes, an additional stanza is required in the configuration file passed to the bbdoc command - here's what it looks like in my example repo:

"auraenabled": {
    "name": "auraenabled",
    "description": "AuraEnabled Class Access", 
    "subdirectory": ".",
    "image": "images/auraenabled.png",
    "suffix":"object",
    "groups": {
        "other": {
            "name":"other", 
            "title":"AuraEnabled Components",
            "description": "All components with Apex controllers"
        }
    }
}

As with other metadata types, you can specify multiple groups to slice up your components into functional areas - I've lumped them all into one group as I only have one component!


Output

The index (home) page for the org report displays a new card for the AuraEnabled metadata:


An error badge is displayed if there are one or more classes used as controllers for components that aren't accessible from any profile or permission set. 

Clicking in to the details shows the groups and errors:


and clicking into the group shows the detail for each component, with classes that aren't accessible highlighted as errors:



Note that if a Lightning Web Component accesses multiple Apex classes, there will be a row for each class with the same component name in the report, as shown above.


Processing

I was quite pleased by how little code I had to write to add support for this:

  • A classes map structure is created in memory, profilesAndPermSetsByClassname, where each entry has the Apex class name as a key and a value object containing lists of permission sets and profiles with access to the class. This is generated by loading all of the profile and permission set metadata and iterating the ClassAccess sections. 

  • The component groups are iterated, and for each entry in the group:

    • If it is an Aura Component, the controller attribute from the <aura:component/> tag is extracted

    • If it is a Lighting Web Component (which can access multiple Apex classes), @salesforce/apex/ lines are identified and the Apex class name extracted. 

      For each class:

      • The entry for the class in the profilesAndPermSetsByClassname map is extracted. If this is null or both of the profile and permission set lists are empty, the classname is added to the error collection displayed on the group page.

      • A row is added to the JavaScript object backing the group page showing the profiles/permission sets that have access, adding the error highlight colour if there are none.


Plug-In

The updated bbddoc Salesforce CLI plug-in providing AuraEnabled support can be found on NPM, and the source code is available in the Github repository. 


Why not use the AuraEnabledScanner?

You absolutely should - I do, to configure access. This works with the AuraEnabledScanner rather than replacing it. As development continues, new classes and components are added to your codebase. The Org Documentor flags up any that don't have appropriate access, and you can then login to an org that your metadata is deployed to, run the scanner and fix up the access. 

The scanner also handles managed classes which the Org Documentor doesn't, as it works against your metadata on disk rather than everything installed in a specific org instance.


Related Posts

Sunday, 14 April 2019

Push Source Faster with the Salesforce CLI and VS Code

Push Source Faster with the Salesforce CLI and VS Code

Introduction

When working with SalesforceDX scratch orgs and VS Code, deploying source is as simple as opening the command palette and choosing the correct command. If there are errors, these are captured in the problems view and I can simply click on the problem to open the file:

Not too arduous right? Actually, it can be, especially when I’ve been on a train or plane and done a bunch of offline work that I’m then trying to deploy. After about a thousand push, fix, repeat cycles, opening the menu and choosing an option seems slow.

Configuring the Default Build Task

Note: this isn’t the way you want to do it when pushing source to a scratch org - it is for the metadata API and it’s also a useful thing to understand. I’m trying to build some suspense really.

As I’ve written before, I can configure any shell command as the default build task in VS Code.

To set up source push as the default, I :

  • open up the command palette and choose ‘Tasks: Configure Default Build Task’
  • choose ‘Create tasks.json file from template'
  • choose ‘Other’

I then replace the contents of the tasks.json file with :

{
    "version": "2.0.0",
    "tasks": [
        {
            "label": "build",
            "command": "sfdx",
            "args": [
                "force:source:push"
            ],
            "group": {
                "kind": "build",
                "isDefault": true
            }
        }
    ]
}

and I can now run the push as the default build task using the key sequence SHIFT+COMMAND+B (on a Mac):

There’s one downside to this and it’s quite a biggie - the problems view is empty so I have to look at the command output to figure the problem and manually locate the problem file and line. Luckily I can solve this relatively easily (it requires regular expressions so obviously there’s a number of attempts to get this right) by adding a problem matcher to parse the output and find any errors:

{
    "version": "2.0.0",
    "tasks": [
        {
            "label": "build",
            "command": "sfdx",
            "args": [
                "force:source:push"
            ],
            "problemMatcher": [
                {
                    "owner": "KAB-apex",
                    "fileLocation": [
                        "relative",
                        "${workspaceFolder}"
                    ],
                    "pattern": {
                        "regexp": "^(.*)  (.*) \\((\\d+):(\\d+)\\)$",
                        "file": 1,
                        "line": 3,
                        "column": 4,
                        "message": 2
                    }
                },
                {
                    "owner": "KAB-lc",
                    "fileLocation": [
                        "relative",
                        "${workspaceFolder}"
                    ],
                    "pattern": {
                            "regexp": "^(.*) \\s \\w*:(\\w*:)(\\d+),(\\d+):\\s(.*)$",
                            "file": 1,
                            "line": 3,
                            "column": 4
                    }
                }
            ],
            "group": {
                "kind": "build",
                "isDefault": true
            }
        }
    ]
}

Now the problems view is populated again, so I have the same functionality with shortcut to kick the deployment off:

The Easy Way

Replacing the default build task is something that I’ve been doing on most of my projects to wire in a node script that manages deployments via the Metadata API. In that scenario I didn’t have any other options, but in this case it felt like I was having to recreate a lot of things to replace standard functionality. It felt like I should be able to create a shortcut to the entry in the command palette, but every time I googled I only got results around binding keyboard shortcuts to tasks.

A long time after I’d given up, I was looking to create a regular keyboard shortcut and opened the menu Code -> Preferences -> Keyboard Shortcuts and absent-mindedly typed ‘SFDX’ into the resulting screen, probably because it reminded me of the command palette:

The set of SFDX commands appeared! Scrolling down I found the source push command, and hovering over this showed a ‘+’ button that allowed me to define a shortcut key sequence - I choose SHIFT+OPTION+P:

Having configured this, I can now push the source simply by pressing that key combination, and I haven't had to replace any of the standard functionality:

As mentioned earlier, I’ve looked for a way to do this a number of times and had no luck - why that is the case I don’t know, but at least I got there in the end! As long as I don’t think about how much time I could have saved, it’s fine.

Related Posts

 

Saturday, 14 January 2017

Salesforce Platform Cache - Expect the Unexpected

Platform Cache - Expect the Unexpected

Cache

Introduction

When the Salesforce Platform Cache appeared, this allowed a lot of home made caching code to be retired. Up until the the only data that was cached was custom settings. (This is in terms of not requiring a trip to the database, there are obviously all sorts of caches at play in the Salesforce platform - reports involving large amounts of data often succeed on the second or third try as some of the data has made it’s way closer to the front end). While custom settings speed up the retrieval of data and save SOQL calls, it was a pretty basic solution, requiring DML to push information into the ‘cache’, and the type of data that could be stored was pretty basic, so caching and rehydrating a complex object meant you had to handle the transformation yourself. 

In this post I won’t be going into the details of getting started with the cache, as the Trailhead module does an excellent job of this already.

Seek, and you may not find it

A key feature of the platform cache is that just because you put something in the cache, it doesn’t mean that it will still be there at a later date, thus you have to be prepared to deal with cache misses. Thus if I store an opportunity keyed by it’s id in a Blue Peter style partition I created earlier:

Cache.OrgPartition oppsPart=Cache.Org.getPartition('Opportunities');
Opportunity opp=[select id, Name, CloseDate from Opportunity where id='0060Y000003AWjG'];
oppsPart.put('0060Y000003AWjG', opp);

I can try to retrieve it in a different request:

Cache.OrgPartition oppsPart=Cache.Org.getPartition('Opportunities');
Opportunity oppFromCache=(Opportunity) oppsPart.get('0060Y000003AWjG');
System.debug('Opp = ' + oppFromCache);

and all things being equal I'll get the following debug output:

12:27:52:158 USER_DEBUG [3]|DEBUG|Opp = Opportunity:{Name=Edge Installation,
Id=0060Y000003AWjGQAW, CloseDate=2014-11-02 00:00:00}

If the opportunity has been booted from the cache for any reason (expired, an evil co-worker removes it) then I'll receive a null rather than the opportunity, and can fetch it from the database.

You have to know what you are asking for

Based on the above, if you are doing a lot of work with opportunities matching a particular criteria, it’s tempting to try to use the cache as a database replacement, storing all of them in there keyed by id:

Cache.OrgPartition oppsPart=Cache.Org.getPartition('Opportunities');
List<Opportunity> opps=[select id, Name, CloseDate from Opportunity];
for (Opportunity opp : opps)
{
	oppsPart.put(opp.id, opp);
}

I can then retrieve these by iterating all of the keys in the partition:

Cache.OrgPartition oppsPart=Cache.Org.getPartition('Opportunities');
List<Opportunity> opps=[select id, Name, CloseDate from Opportunity];
for (Opportunity opp : opps)
{
	oppsPart.put(opp.id, opp);
}

which gives me the following output:

12:42:40:176 USER_DEBUG [6]|DEBUG|Opp = Opportunity:{Name=Edge SLA,
Id=0060Y000003AWjHQAW, CloseDate=2014-11-02 00:00:00}
12:42:40:180 USER_DEBUG [6]|DEBUG|Opp = Opportunity:{Name=United Oil
Installations, Id=0060Y000003AWjFQAW, CloseDate=2014-11-02 00:00:00}
...
12:42:40:261 USER_DEBUG [6]|DEBUG|Opp = Opportunity:{Name=Burlington Textiles
Weaving Plant Generator, Id=0060Y000003AWjOQAW, CloseDate=2014-11-02 00:00:00}

At first glance this looks fine, but there is a huge issue with this approach - I have no idea whether this is the full collection of opportunities that I cached. Entries might have been evicted, even while I was iterating the opportunities I’d queried if the partition filled up, or an evil co-worker may remove a couple and then add them back later so that I have no idea why they weren’t processed.

Unlike executing  SOQL query, all I can be sure of is that I’ve retrieved all of the opportunities that remain in the cache, which may bear very little relation to the contents of the database. Clearly another approach is required. 

Cache collections as a single entry

In this scenario, rather than storing each object as an entry the solution is to store the entire collection as a single entry:

Cache.OrgPartition oppsPart=Cache.Org.getPartition('Opportunities');
List<Opportunity> opps=[select id, Name, CloseDate from Opportunity];
oppsPart.put('all', opps);

retrieving it in a separate request as follows:

Cache.OrgPartition oppsPart=Cache.Org.getPartition('Opportunities');
List<Opportunity> oppsFromCache=(List<Opportunity>) oppsPart.get('all');
for (Opportunity opp : oppsFromCache)
{
	System.debug('Opp = ' + opp);
}

In this case, if my opportunities have been evicted from the cache I’ll receive a null response and can re-query them from the database. Of course this doesn’t stop an evil co-worker from overwriting the ‘all' entry in the cache with a different collection of opportunities - if this continues to be a problem then a session cache partition, which is tied to a specific user, is probably a better option.

More Information

Sunday, 29 July 2012

London SFDC User Group Technical Architect Talk

This week I gave a talk at the SFDC London User Group meetup that took place on 26th July at the Skills Matter eXchange.

A recording of the talk is available from the Skills Matter web site, and the slide deck is available for download here.


If you have any questions on the presentation, slide deck or the technical architect certification in general, please post them to the comments section below and I'll lift them up into a Q&A section in this post.