Tweet |
Introduction
Yeah, I know, another week another plug-in. Unfortunately this is likely to
continue for some time, as my plug-ins are only really limited by my
imagination. I think this one is pretty cool though and I hope, dear reader,
you agree.
At Dreamforce 2019, Salesforce debuted Evergreen - a technology to allow
Heroku microservices to be invoked from Apex (and other places) seamlessly.
One of the Evergreen use cases is creating a PDF from Salesforce data, and one
of the strap lines around this is that with Evergreen you have access to the
entire Node package ecosystem. Every time I heard this I'd think that's true,
but CLI plug-ins have the same access, as they are built on node, and like
Evergreen there is no additional authentication required.
Essentially this is the opposite of Evergreen - rather than invoking something
that lives on another server that returns a PDF file, I invoke a local command
that writes a file to the local file system. I toyed with the idea of calling
this plug-in Deciduous, but decided that was but of a lengthy name to type,
and I'd have to explain it a lot! (and yes I know that Evergreen is a lot more
than creating a PDF, but I liked the image so decided to go with it).
The plug-in is available on NPM - you can install it by executing : sfdx plugins:install bbpdf
EJS Templates
There are a couple of ways to create PDFs with node - using something like
PDFKit to add the individual
elements to the document, or my preferred route - generating the document from
an HTML page. And if I'm
generating HTML from Salesforce data or metadata, I'm going to be using
EJS templates.
I'm not going to go into too much detail in this post about the creation of
the HTML via EJS, but if you want more information check out my blog post on
how I used it in my
bbdoc plug-in
to create HTML documents from Salesforce metadata.
My plug-in requires two flags to specify the template. The name of the
template itself, specified via the --template/-t flag, and the templates
directory, specified via the --template-dir/-d flag. The reason I need the
directory is that any css or images required by the template will be specified
relative to the template's location. You can see this working by cloning the
samples repo and trying it out.
Salesforce Data
My plug-in provides two mechanisms to retrieve data from Salesforce.
Query Parameter
This is for the simple use case where the template requires a single record.
The query to retrieve the record is supplied via the
--query/-q flag.
Querying Salesforce data from a plug-in is quite straightforward, I specify
that I need a username which guarantees me access to an org:
protected static requiresUsername = true;
then I create a connection:
const conn = this.org.getConnection();
and finally I run the query, based on the supplied parameter, and check
the results:
const result = await conn.query<object>(this.flags.query); if (!result.records || result.records.length <= 0) { ... }
Once I have the record I need to pass in the Salesforce data in an object to
the template, with the property name expected by the template, and this is
provided by the user through the --sobject/-s
flag.
This is a bit clunky though, so here's the preferred route.
Query File
A query file is specified via the
--query-file/-f
flag. This is a JSON format file containing an object with a property
per query:
{ "contact": { "single": true, "query": "select Title, FirstName, LastName from Contact where id='00380000023TUDeAAO'" }, "account": { "single": true, "query": "select id, Name from Account where id='0018000000eqTV7AAM'" } }
The property name is the name that will be supplied to the EJS template - in
this example there will be two properties passed to the template - contact and
account. The single property specifies whether the query will result in
a single record or an array, and the query property specifies the actual SOQL
query that will be executed. Using this mechanism, any number of records or
arrays of records can be passed through to the template.
Generating the PDF
In a recurring theme in this post, there are a couple of ways to generate the
PDF from the HTML. The simplest in terms of setup and code is to use the
html-pdf
package, which is a wrapper around
PhantomJS. While simple,
this is sub-optimal - the package hasn't been updated for a while, development
on Phantom is suspended and when I added this to my plug-in, npm reported a
critical vulnerability, so clearly another way would be required.
The favoured mechanism on the interwebs was to use
Puppeteer
(headless chromium node api), so I set about this. After some hoop jumping
around creating temporary directories and copying the templates and associated
files around, it wasn't actually too bad. I loaded the HTML from the file that
I'd created and then asked for a PDF version of it:
const page = await browser.newPage(); await page.goto('file:///' + htmlFile, {waitUntil: 'networkidle0'}); const pdf = await page.pdf({ format: 'A4' });
One area I did go off-piste slightly was not using chromium bundled with
puppeteer, instead I rely on the user defining an environment variable for
their local installation of chrome and I use that. Mainly because I didn't
want my plug-in to have to download 2-300 Mb before it could be used. This may
cause problems down the line, so caveat emptor.
The environment variable in question is PUPPETEER_EXECUTABLE_PATH. Here
are the definitions from my Macbook Pro running MacOS Catalina:
export PUPPETEER_EXECUTABLE_PATH="/Applications/Google
Chrome.app/Contents/MacOS/Google Chrome"
and my Surface Pro running Windows 10:
setx PUPPETEER_EXECUTABLE_PATH "C:\Program Files
(x86)\Google\Chrome\Application\chrome.exe"
The PDF is then written to the location specified by the
--output/-o flag.
The next hoop to jump through came up when I installed the plug-in - Puppeteer
is currently at 3.3.0 which requires Node 10.18.1 but the Salesforce CLI node version is 10.15.3. Luckily I was able to switch to Puppeteer version 2.1.1 without introducing any
further issues.
Samples Repo
There's a lot of information in this post and a number of flags to wrap your
head around - if you'd like to jump straight in with working examples, check
out the
samples repo. This has a couple of example commands to try out - just make sure you've
set your chrome location correctly - and the PDFs that were generated when I
ran the plug-in against my dev orgs.
Related Posts
- The Plug-In on NPMJS
- Source for the Plug-In
- Samples Repo
- Documentor Plug-In - Triggers (and Bootstrap)
- Documenting from the metadata source with a Salesforce CLI Plug-in - Part 5
- Documenting from the metadata source with a Salesforce CLI Plug-in - Part 4
- Documenting from the metadata source with a Salesforce CLI Plug-In - Part 3
- Documenting from the metadata source with a Salesforce CLI Plug-In - Part 2
- Documenting from the metadata source with a Salesforce CLI Plug-In - Part 1
- Offline mobile app template plug-in Dreamforce 18 session
- Salesforce CLI Play-by-Play
- Salesforce CLI Cheat Sheet