<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>so-called programmer</title>
    <description>My personal blog about web development, mainly Salesforce related.
</description>
    <link>http://seanpat09.github.io/</link>
    <atom:link href="http://seanpat09.github.io/feed.xml" rel="self" type="application/rss+xml"/>
    <pubDate>Mon, 12 Dec 2022 18:27:46 +0000</pubDate>
    <lastBuildDate>Mon, 12 Dec 2022 18:27:46 +0000</lastBuildDate>
    <generator>Jekyll v3.9.2</generator>
    
      <item>
        <title>Technical Debt Is The Wrong Term - We Should Call it Code Rot</title>
        <description>&lt;h2 id=&quot;what-is-technical-debt&quot;&gt;What is technical debt?&lt;/h2&gt;

&lt;p&gt;The term technical debt gets thrown around at any company that maintains any sort of code base, often used to explain to stakeholders the need to dedicate resources to refactoring. Generally I hear it summarized as expediting delivery by intentionally writing imperfect code, thus saving time. Eventually this debt has to be paid back eventually - either in development time to address it proactively or in the time to recover when things go wrong. However, financial debt has always felt like a poor comparison and at best miscommunicates the problem and at worst misleads the audience.&lt;/p&gt;

&lt;h2 id=&quot;financial-debt-is-a-bad-analogy&quot;&gt;Financial debt is a bad analogy&lt;/h2&gt;
&lt;p&gt;Financial debt has little in common with what we call technical debt. Financial debt has to be paid back eventually, usually in a very defined schedule with specific consequences for not doing so. Code is not like that all; there is no “due date” for technical debt and it may never come. The internet is held together with scripts written in a weekend that have been running for years. You also usually don’t know the consequences of not refactoring. Sometimes the market for the product just ends and the code gets thrown away, never needing to be refactored.&lt;/p&gt;

&lt;p&gt;There is also an element of intentionality when it comes to financial debt. You take out a loan with the goal of accomplishing something that has a higher rate of return than the interest on the loan (well, in theory, but that’s another blog post). With code, there are occasions where you intentionally make that decision. For example, I often work on an MVP and see an opportunity to use a design pattern to make a feature more scalable, but opt not to go that path because solving for scale isn’t needed at the time. If that need for scale comes suddenly, it will be harder to implement on that tighter timeline in the future, but we don’t handle it now because if we don’t deliver, scale won’t be a problem.  This is what I think most of people consider to be technical debt. However, most technical debt is not intentional and as such we shouldn’t call it debt. We should call it “rot.”&lt;/p&gt;

&lt;h2 id=&quot;code-rot&quot;&gt;Code Rot&lt;/h2&gt;
&lt;p&gt;The second code is delivered it has an expiration which could come due to known future scenarios and unknown external influences. For example:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;A security vulnerability could be discovered, requiring a rewrite several pieces of functionality&lt;/li&gt;
  &lt;li&gt;Version upgrades could deprecate functionality that you depended on, requiring a work around&lt;/li&gt;
  &lt;li&gt;Your product could start to be used in unexpected ways, creating bottlenecks in unexpected places&lt;/li&gt;
  &lt;li&gt;Even lack of documentation causes code rot, expiring immediately the day the one person that understands everything decides to leave. (There’s no such thing as legacy code, just code that you didn’t write and now have to maintain!)&lt;/li&gt;
  &lt;li&gt;You could have just plain not anticipated something. It happens all the time and should be expected&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And kind of like food, there’s no defined expiration date. Sure, you know that the milk will go bad after a few weeks, but how is the flour after a year? And honey doesn’t go bad, right? If you aren’t careful, one day you will open the fridge and it will reek and you’ll have no idea where it’s coming from.&lt;/p&gt;

&lt;h2 id=&quot;keep-your-code-fresh&quot;&gt;Keep your code fresh&lt;/h2&gt;
&lt;p&gt;If you want to keep your code fresh, you need to look through the fridge regularly. A really good practice is to get rid of functionality you don’t need. Code that doesn’t exist doesn’t go bad! The idea of “what’s the harm in keeping it?” is a poor one because all code must be maintained.&lt;/p&gt;

&lt;p&gt;Log everything. You should have technical notes for each of your features, either in comments or in separate documentation. Call out places that are potential technical debt so that future people (including yourself) have an idea why something is the way it is.&lt;/p&gt;

&lt;p&gt;Speaking of technical documentation, make sure it exists! I am a huge fan of code comments on methods explaining what’s going on and the intent. Runbooks are great too, i.e. documentation of how a feature is expected to be used. Keep your solution docs archived and make sure you have a system where your commits can be linked to a change request. Historical context is key to making sure you are addressing rot correctly.&lt;/p&gt;

&lt;h2 id=&quot;code-rot-is-everyones-problem&quot;&gt;Code rot is everyone’s problem&lt;/h2&gt;
&lt;p&gt;I often see technical debt used as a way to deflect on a problem - with technical stakeholders saying they don’t get enough time and resources to address issues and non-technical stakeholders saying they don’t have enough metrics to measure success. Cleaning out the code fridge is everyone’s problem.&lt;/p&gt;

&lt;p&gt;If you are technical contributer, it is on you to track where the rot is, what needs to be done, and take a swing at some predications on the consequences of not doing so. If you’re a non-technical contributer, you need to consider that this time is table stakes for successfully delivering a product and be ruthless when it comes to prioritizing new work. If you cannot tell the technical stakeholders what the consequences of not adding a feature is, then the feature doesn’t need to be added.&lt;/p&gt;

&lt;p&gt;This is all easier said than done - this is both a technical problem and a cultural one. I’m hoping if that if we start using a term that better fits the reality, then maybe we’ll be more successful at communicating it’s importance to the code we work on.&lt;/p&gt;
</description>
        <pubDate>Mon, 12 Dec 2022 08:00:00 +0000</pubDate>
        <link>http://seanpat09.github.io/2022/12/12/technical-debt-redux.html</link>
        <guid isPermaLink="true">http://seanpat09.github.io/2022/12/12/technical-debt-redux.html</guid>
        
        <category>technical debt</category>
        
        
      </item>
    
      <item>
        <title>Hosting your own Mastodon server</title>
        <description>&lt;h2 id=&quot;why-host-your-own-server&quot;&gt;Why Host Your Own Server&lt;/h2&gt;

&lt;p&gt;With Mastodon becoming more popular, I wanted to see what was invovled in creating my own server. I figured if it didn’t work out I could join one of the bigger servers later, either by creating a new profile, or by migrating the profile from my own server. There are a few reasons that I think make hosting your own server a good fit for you.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;You want to have more control over your own content on the internet. While you may be able to move Mastodon servers, only your profile and followers go with you, NOT your posts&lt;/li&gt;
  &lt;li&gt;You want to experiment with the technology itself&lt;/li&gt;
  &lt;li&gt;You already have a bunch of people you want to follow and that want to follow you. Without this, it may be hard for your content to reach other federated timelines if people aren’t able to find you easily.&lt;/li&gt;
  &lt;li&gt;You want to host a community and are committed to maintaining and moderating your server.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you are going to open up registrations, that last piece is important. While people can migrate out of your server should you decide not to maintain the server anymore, I think it’s important to be upfront with your intentions so your users aren’t suddenly scrambling to find somewhere else to go.&lt;/p&gt;

&lt;h2 id=&quot;still-want-to-host-a-server&quot;&gt;Still Want to Host a Server?&lt;/h2&gt;

&lt;p&gt;There are several options for hosting. There are managed products such as &lt;a href=&quot;https://masto.host&quot;&gt;masto.host&lt;/a&gt; that you can just sign up and they manage the infrastruture for you. Something like &lt;a href=&quot;https://www.cloudron.io/&quot;&gt;cloudron.io&lt;/a&gt; gives you a little more control in that you own the machine itself (even if just in the cloud), but they manage installing the product on your machine. I don’t know much else about these services, but they are available if you are not interested in diving in the command line.&lt;/p&gt;

&lt;p&gt;I’m interested in the code itself, but was also intimidated by the idea of installing from source. Luckily Digital Ocean had something to meet my needs.&lt;/p&gt;

&lt;h2 id=&quot;my-setup-experience&quot;&gt;My setup experience.&lt;/h2&gt;

&lt;p&gt;I opted to host on Digital Ocean after I found their 1 click app. If you’d like, you can use &lt;a href=&quot;https://m.do.co/c/ff49fa0cafe3&quot;&gt;my referal code&lt;/a&gt; to get some Digital Ocean credits for yourself and for me if you don’t have an account yet. Once you have an account, go to the &lt;a href=&quot;https://marketplace.digitalocean.com/apps/mastodon&quot;&gt;1 click app page&lt;/a&gt; and you can create a droplet. When you do, make sure to click a droplet size that has at least 2GB of RAM. I was running into a lot of issues in with a 1GB droplet and am currently running at 80% in the 2GB droplet.&lt;/p&gt;

&lt;p&gt;During that setup, make sure to choose to upload an SSH key. Here’s &lt;a href=&quot;https://docs.digitalocean.com/products/droplets/how-to/add-ssh-keys/&quot;&gt;instructions on how to generate an SSH key&lt;/a&gt;. I find it’s more secure than using a password.&lt;/p&gt;

&lt;p&gt;While that is setting up, we’ll need a few other things.&lt;/p&gt;

&lt;p&gt;First I needed a domain name. I used &lt;a href=&quot;https://name.com&quot;&gt;name.com&lt;/a&gt; to register mine, but feel free to use whatever provider you want. After getting my domain, I needed to configure the domain to in Digital Ocean. Here’s the &lt;a href=&quot;https://docs.digitalocean.com/products/networking/dns/how-to/add-domains/&quot;&gt;link to the official documentation&lt;/a&gt; from Digital Ocean on how to setup your domain and DNS records.&lt;/p&gt;

&lt;p&gt;I also needed an email provider. I tried to set up my your own email server on the droplet with the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sendmail&lt;/code&gt; command, but my email were not being delivered to gmail. Whether it was an issue with my setup or gmail just blocking the emails, I’m not sure, but I eventually gave up. SendGrid has a free tier, which I think should be enough for a single user server. If you’re planning to open registrations you might want to shop around to find a provider that fits your budget. If you use SendGrid, follow &lt;a href=&quot;https://docs.sendgrid.com/ui/account-and-settings/api-keys&quot;&gt;the instructions in their documentation to set up an API Key&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;With that all set up, let’s ssh into the droplet and install Mastodon. Here are &lt;a href=&quot;https://docs.digitalocean.com/products/droplets/how-to/connect-with-ssh/&quot;&gt;instructions on connecting with SSH&lt;/a&gt;. Once you are connected, you’ll be greeted by the setup wizard and some prompts. Here’s what I put to configure my server. When things went wrong I was able to use disconnect from the droplet and reconnect to restart the wizard. The wizard appears to keep running until you succesfully complete it:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Welcome to the Mastodon first-time setup!
Domain name: forcedconversation.com
Do you want to store user-uploaded files on the cloud? No
SMTP server: smtp.sendgrid.net
SMTP port: 587
SMTP username: apikey
SMTP password: &amp;lt;my_send_grid_api_key&amp;gt;
SMTP authentication: plain
SMTP OpenSSL verify mode: none
E-mail address to send e-mails &quot;from&quot;: Sean &amp;lt;no-reply@forcedconversation.com&amp;gt;
Send a test e-mail with this configuration right now? no
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;I didn’t want to configure S3 or some other cloud host, so all user uploaded files will live on the droplet. It’s just me so I figured it’d be ok. The only other thing of note is that the SMTP username is literally &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;apikey&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;After completing those prompts I was able to create my admin account:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Great! Saving this configuration...
Booting up Mastodon...
It is time to create an admin account that you'll be able to use from the browser!
Username: squizzleflip
E-mail: sean.cuevo@gmail.com
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Next I was given my initial password, and prompted to setup Let’s Encrypt for SSL&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;You can login with the password: YOUR-PASSWORD-WILL-BE-HERE
The web interface should be momentarily accessible via https://forcedconversation.com/
Launching Let's Encrypt utility to obtain SSL certificate...
Saving debug log to /var/log/letsencrypt/letsencrypt.log
Plugins selected: Authenticator webroot, Installer None
Enter email address (used for urgent renewal and security notices) (Enter 'c' to cancel): sean.cuevo@gmail.com
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This failed a BUNCH of times - turns out when I set up the DNS recodrs I had a typo in my domain name. A frustrating error, but at least not a complicated one.&lt;/p&gt;

&lt;p&gt;After that I had to update certificates:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;sudo apt install ca-certificates
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Lastly I needed to add &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;127.0.0.1 localhost forcedconversation.com&lt;/code&gt; to the file &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/etc/hosts&lt;/code&gt;.
And that was it! I was up and running!&lt;/p&gt;

&lt;h2 id=&quot;immediate-upgrades&quot;&gt;Immediate Upgrades&lt;/h2&gt;

&lt;p&gt;I started posting on my instance and one of the first messages I got was “How did you manage upgrades?” to which I thought “Upgrades? I just installed it, what would I need to upgrade?” Turns out the Digital Ocean 1-click-app is set to Mastodon version 3.1.3, while the latest release is 3.5.3. COOL.&lt;/p&gt;

&lt;p&gt;I connected to my droplet again and in another monitor and opened up the &lt;a href=&quot;https://docs.joinmastodon.org/admin/upgrading/&quot;&gt;upgrade documentation&lt;/a&gt;. Fortunately you can update directly from 3.1.3 to the latest version, but the documentation does warn about making sure to follow any intermediary upgrade steps. For example, if version 3.2.1 has a unique upgrade setup, you’ll probably need to do that as well. I opened up all the releases and found I needed to do the following:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Run &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;apt install shared-mime-info&lt;/code&gt; as root&lt;/li&gt;
  &lt;li&gt;Update ruby to 3.0.3 with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;RUBY_CONFIGURE_OPTS=--with-jemalloc rbenv install 3.0.3&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;Run &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bundle install&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;Run &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;yarn install&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;Run &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SKIP_POST_DEPLOYMENT_MIGRATIONS=true RAILS_ENV=production bundle exec rails db:migrate&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;Run &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;RAILS_ENV=production bundle exec rails assets:clobber assets:precompile&lt;/code&gt;
    &lt;ul&gt;
      &lt;li&gt;The docs don’t include the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;assets:clobber&lt;/code&gt; part, which caused me to run into &lt;a href=&quot;https://github.com/mastodon/mastodon/issues/15847&quot;&gt;this issue&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;Run &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;systemctl restart mastodon-sidekiq&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;Run &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;systemctl restart mastodon-web&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;Run &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;RAILS_ENV=production bin/tootctl cache clear&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;Run &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;RAILS_ENV=production bundle exec rails db:migrate&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;Run &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;systemctl restart mastodon-sidekiq&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;Run &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;systemctl restart mastodon-web&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once that was complete I was finally upgraded the latest version. Hopefully following those steps will make this a little more straightforward if you try this.&lt;/p&gt;

&lt;p&gt;Lastly I set the server to single user mode by adding &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SINGLE_USER_MODE=true&lt;/code&gt; in the file &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/home/mastodon/live/.env.production&lt;/code&gt;. This disable registrations and points the domain directly to my profile.&lt;/p&gt;

&lt;h2 id=&quot;whats-next&quot;&gt;What’s Next?&lt;/h2&gt;

&lt;p&gt;Things seem fairly stable right now, but I’m actually not sure what more work will be required. After a few months and some upgrades, I may decide to open up some limited registrations. If you want to try this yourself and have any questions, please feel free to tag in a post on &lt;a href=&quot;https://forcedconversation.com&quot;&gt;@squizzleflip@forcedconversation.com&lt;/a&gt;! Good luck and happy tooting!&lt;/p&gt;
</description>
        <pubDate>Tue, 08 Nov 2022 08:00:00 +0000</pubDate>
        <link>http://seanpat09.github.io/2022/11/08/host-your-own-mastodon-server.html</link>
        <guid isPermaLink="true">http://seanpat09.github.io/2022/11/08/host-your-own-mastodon-server.html</guid>
        
        <category>mastodon</category>
        
        <category>server</category>
        
        
      </item>
    
      <item>
        <title>Across the Fediverse</title>
        <description>&lt;h2 id=&quot;the-fediverse&quot;&gt;The Fediverse&lt;/h2&gt;

&lt;p&gt;I’ll start with I kind of hate the term “fediverse”, but I do like the concept! If you’re not familiar with a federated network, Mastodon, or how this “bird site replacement” works, I’ll try to explain in as few words possible. A federated network is distributed amongst separate servers that communicate with each other using a common protocol or “language” (called &lt;a href=&quot;https://en.wikipedia.org/wiki/ActivityPub&quot;&gt;ActivityPub&lt;/a&gt; if you want to dig into the details). So while your account lives in one server, because of the common language across servers, you are able to follow and reply to people on different servers.&lt;/p&gt;

&lt;p&gt;Mastodon is one type of software that uses a shared language to create a Twitter-like replacement, and because of the federated network, all of these Mastodon servers are able to interact with each other. You could join any server and you’d be able to follow/reply to people on other servers fairly seemlessly. There are also other types of software like Pleroma or Pixelfed (which is like instagram), that also implement the same protocol, so you can even follow people using those platforms and vice versa!&lt;/p&gt;

&lt;p&gt;You can join any server you want, and if you’re unsure I’d say just start with one of the bigs ones like &lt;a href=&quot;https://mastodon.social&quot;&gt;mastodon.social&lt;/a&gt;. The server you join doesn’t matter in the sense that you can communicate with people on other servers if you want, but it does matter in that it is where your posts will live and more importantly how the server is moderated.&lt;/p&gt;

&lt;p&gt;Whoever is hosting a Mastodon server has complete control over that server. The admins can read any post on the server, including DMs. This is the case with something like Twitter, but the perception of trust can be different when it’s just a few hobbyists running a server as opposed to a giant corporation. Those admins also have control over which servers can interact with their server. For example, an admin on a Star Wars Mastodon server could block that servers interactions with a Star Trek server. So even though the Star Trek users are following the Star Wars users (and vice versa), the admin can decide that no Star Trek server posts will appear on their timeline.&lt;/p&gt;

&lt;h2 id=&quot;dunbars-number-and-a-better-online-experience&quot;&gt;Dunbar’s Number and a Better Online Experience&lt;/h2&gt;

&lt;p&gt;Having a distributed network means smaller user bases for each server. Instead of having to moderate the interactions and posts of millions of users on a single server, moderators only have to monitor the thousands or even just hundreds on their own server, while also having the ability to block communities in mass that don’t align with the values of theirs. Additionally, if an asshole suddenly becomes the owner of the instance you’re on, the fediverse’s portability allows you to leave a server and bring your followers with you to a server that you more align with.&lt;/p&gt;

&lt;p&gt;The concept of many, small servers really appeals to me because I think it’s the best way to scale social networking that prioritizes safety because of the theory behind Dunbar’s Number. For those not familiar, &lt;a href=&quot;https://en.wikipedia.org/wiki/Dunbar%27s_number&quot;&gt;Dunbar’s Number&lt;/a&gt; is the concept that an individual has a cognitive limit on the number of people they can maintain a stable social relationship, that number suggested to be around 150. To me, that means that I could potentially host a server of about 100 or so users and successfully priotize the safety of their interactions in my server. And because of the federated network, those users are not isolated to my server, they can freely interact with others in other servers. Again, if some asshat decides to buy his way into owning another server and it becomes a toxic hellscape, I can easily block that server’s posts from showing up in my server’s timeline.&lt;/p&gt;

&lt;p&gt;If you’re familiar with Discord, which while not federated, you’ll see that it has a very similar feel. My online interactions on Discord are by far the least toxic and most meaningful in large part because of the small and separated nature of the servers.&lt;/p&gt;

&lt;h2 id=&quot;hosting-my-own-instance&quot;&gt;Hosting My Own Instance&lt;/h2&gt;

&lt;p&gt;Currently I host a single user instance on &lt;a href=&quot;https://forcedconversation.com&quot;&gt;forcedconversation.com&lt;/a&gt;. I’m not sure if I will open registrations; I’m not sure what is going to be required of me to keep this thing running, what the costs will end up looking like, what moderating posts will be like, or even if there is interest to join. However, I am intrigued by the idea, especially if there were others that would be interested in helping admin and moderate the server. For now feel free to come follow me &lt;a href=&quot;https://forcedconversation.com/@squizzleflip&quot;&gt;@squizzleflip@forcedconversation.com&lt;/a&gt; and if you would be interested please send me a message! I’m really excited about this technology and I hope more people start to jump on board. This feels like what the future of social networking should be!&lt;/p&gt;

</description>
        <pubDate>Fri, 04 Nov 2022 08:00:00 +0000</pubDate>
        <link>http://seanpat09.github.io/2022/11/04/across-the-fediverse.html</link>
        <guid isPermaLink="true">http://seanpat09.github.io/2022/11/04/across-the-fediverse.html</guid>
        
        <category>mastodon</category>
        
        <category>fediverse</category>
        
        
      </item>
    
      <item>
        <title>A Decade of Code</title>
        <description>&lt;h2 id=&quot;wax-nostalgic&quot;&gt;Wax Nostalgic&lt;/h2&gt;

&lt;p&gt;Ten years ago, I walked into the Bluewolf office in NYC that overlooked Madison Square Park. I passed someone working in what looked like a phone booth, and I remember thinking, “wow that person must be super important, they have their own office!” After the tour and some brief intros, I was shuttled into that phone booth where my laptop awaited me. Turns out that person started the week before me and there just was no more desk space.&lt;/p&gt;

&lt;p&gt;My first project was to help maintain a custom built CMS built using Ruby on Rails for some non-profit in the city. I don’t remember much of about it but I do remember breaking the staging environment, blocking the client from making any changes. Luckily they didn’t notice while my mentor and I panicked to figure out what we broke. Eventually that client contract ended and didn’t renew (for unrelated reasons, I assume), which meant I needed to start learning Salesforce development. 10 years and 6 jobs later, here I am. And if you’d indulge me for a bit, I’d like to share some lessons from along the way.&lt;/p&gt;

&lt;h2 id=&quot;in-no-particular-order-lessons-from-a-decade-of-code&quot;&gt;In No Particular Order, Lessons from a Decade of Code&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;Learning vim would probably make me very productive, or at least feel that way. I’ve given up every time I’ve tried&lt;/li&gt;
  &lt;li&gt;Everything is a form. It’s just inputs that you’re capturing for some other system to consume. This gives me existential dread sometimes&lt;/li&gt;
  &lt;li&gt;Rebuilding is almost never worth it, until it is&lt;/li&gt;
  &lt;li&gt;Being very responsive to every slack message will get you lots of praise. It will also quickly burn you out&lt;/li&gt;
  &lt;li&gt;I used to think being able to solve problems quickly during live meetings was a skill. It’s actually a a detriment that prevents other people from contributing that leads to poorly thought out solutions and should be discouraged&lt;/li&gt;
  &lt;li&gt;The value of meeting plummets after adding more than 3 people. I promise you it should just be an email&lt;/li&gt;
  &lt;li&gt;No new feature cycle, from ideation to delivery, should be less than a day. I don’t care how small that change is, it needs to be prioritized like everything else or else you’ll constantly be drowning in “quick changes”&lt;/li&gt;
  &lt;li&gt;Processes not only protect the product, but they also protect you. However they should also work for you, not the other way around&lt;/li&gt;
  &lt;li&gt;A sustainable pace is critical for any role that you want to last for more than 2 years&lt;/li&gt;
  &lt;li&gt;Burnout takes months to recover from. It’s not something you can just take a long weekend to fix, especially if you don’t address the cause when you get back&lt;/li&gt;
  &lt;li&gt;Don’t derive your value or your passion from work. It will only hurt you&lt;/li&gt;
  &lt;li&gt;Working from home is the best and I would NEVER work in an office again if I had the choice&lt;/li&gt;
  &lt;li&gt;Version control is the single most important piece of technology if you’re writing code. It’s the bare minimum and I refuse to work in any environment that doesn’t have it&lt;/li&gt;
  &lt;li&gt;Design patterns should serve as inspiration, not as rules to be followed dogmatically&lt;/li&gt;
  &lt;li&gt;The agile manifesto itself is really good - it’s a focus on building a process that works for those involved, favoring a frequent feedback loop. If you ever say “that’s not Agile”, you’re missing the point&lt;/li&gt;
  &lt;li&gt;Documentation doesn’t get enough love. Code can’t document itself - the code can only do what’s written, it’s bad at showing intent&lt;/li&gt;
  &lt;li&gt;You will NEVER beat Excel. You should embrace it and integrate with a spreadsheet instead&lt;/li&gt;
  &lt;li&gt;You’ll generally make more money switching jobs than a promotion&lt;/li&gt;
  &lt;li&gt;Always ask for more than what you’re making. If a recruiter asks what you’re looking, add 20% over what your currently making. If they ask how much you’re making, lie&lt;/li&gt;
  &lt;li&gt;Transactional data should be separate from analytics data. Specifically, putting analytics data on your sObjects like Account is an anti-pattern&lt;/li&gt;
  &lt;li&gt;Capture events that happened, not timestamps that imply something. FIGHT ME.&lt;/li&gt;
  &lt;li&gt;I hate pair programming and I think it should be ok if you never want to do it&lt;/li&gt;
  &lt;li&gt;Favor async over synchronous collaboration&lt;/li&gt;
  &lt;li&gt;Giving people the chance to hit that flow state is the cheapest way to make your employees happy&lt;/li&gt;
  &lt;li&gt;Invest in good tools (chairs, monitors, desk, etc). You’re using them for at least a third of your day&lt;/li&gt;
  &lt;li&gt;You don’t need a side project. If you have one, don’t hesitate to work on it during the work week. You’re getting your work done right? So no one should care and it counts as professional development&lt;/li&gt;
  &lt;li&gt;Take the time to experiment with technology. On company time.&lt;/li&gt;
  &lt;li&gt;Keep your personal projects off company assets. I don’t know how enforceable those technology tranfers are and I don’t want to find out&lt;/li&gt;
  &lt;li&gt;Recognize the signs of burnout - no one is going to do it for you, you have to advocate for yourself&lt;/li&gt;
  &lt;li&gt;Getting really good at one thing can be very lucrative as opposed to chasing the latest technologies&lt;/li&gt;
  &lt;li&gt;Max out company matching on your retirement account - it’s free money&lt;/li&gt;
  &lt;li&gt;You don’t have to be passionate about your job to be good at it&lt;/li&gt;
  &lt;li&gt;Your job is not your family. You can (and probably will) develop friendships on a personal level with your coworkers naturally, but the idea that your company can be family is a lie&lt;/li&gt;
  &lt;li&gt;You will have trash managers. Leave if you can&lt;/li&gt;
  &lt;li&gt;You will have incredible managers who actually do feel family. Thank them often and let them know&lt;/li&gt;
  &lt;li&gt;Just because you’re good at your job doesn’t mean you’d be good at managing others doing that job, or that you’d even like management&lt;/li&gt;
  &lt;li&gt;Think often what you like about a job. Lean into that. You don’t have to love your job, but maximizing the parts that bring you joy helps&lt;/li&gt;
  &lt;li&gt;If software development didn’t pay like it does, I absolutely would do something else, and that’s ok if you feel that way&lt;/li&gt;
  &lt;li&gt;Forgive those that came before you. You don’t know what time pressures, uncontrollable circumstances, etc. that led to the state of the codebase. Don’t get angry, just take this into account when you pad your estimates&lt;/li&gt;
  &lt;li&gt;Estimates can be pretty arbitrary, but they become less so the more you do them, and they are very important when it comes to planning&lt;/li&gt;
  &lt;li&gt;Don’t be the only person that can do your job, there should always be at least two so that you can go on vacation&lt;/li&gt;
  &lt;li&gt;Be kind to each other. Avoid toxic interactions and embrace those that bring you joy. This is really hard&lt;/li&gt;
  &lt;li&gt;Know which battles to fight. When you stand your ground it’ll be really apparent how much it matters&lt;/li&gt;
  &lt;li&gt;You are worth it. You are worth that promotion, that raise, that time off. You deserve to be in that job, you know what you are doing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I could probably do this all day, but this is enough for now. Here’s to 10 more years of code! Thank you everyone that helped get me here because I certaintly did not do it alone. And if you’re starting your journey, I hope I can help you as well.&lt;/p&gt;
</description>
        <pubDate>Wed, 19 Oct 2022 08:00:00 +0000</pubDate>
        <link>http://seanpat09.github.io/2022/10/19/a-decade-of-code.html</link>
        <guid isPermaLink="true">http://seanpat09.github.io/2022/10/19/a-decade-of-code.html</guid>
        
        <category>career advice</category>
        
        
      </item>
    
      <item>
        <title>Can we trick salesforce into doing reportable dynamic rollups?</title>
        <description>&lt;h2 id=&quot;rollups-why-is-it-always-rollups&quot;&gt;Rollups. Why is it always rollups.&lt;/h2&gt;

&lt;p&gt;I hate rollups. I hate building them. I hate how they slow down things in Salesforce. I hate how they can accidentally block users from editing records. And I especially hate debugging them. “Why is this roll up field wrong?” I don’t know, there are so many points of failure when it comes to a roll up. Maybe the job hasn’t run yet and someone just added another record. Maybe an error was thrown and a record wasn’t accounted for. But sure, I’ll throw away a few hours to answer your question and by the time I have an answer the rollup corrected itself. I digress.&lt;/p&gt;

&lt;p&gt;And I say all this as a contributor to the Declarative Rollup Summary open source project. I get why we need rollups. Not every org has access to an analytics tool and is limited to Salesforce reporting. And sometimes an orgs needs to answer the question “How many Account records have donated more than $1000?” and the person asking the question needs to do this with a report. So you build a rollup summary because the alternative is building a custom reporting tool and that’s it’s own set of problems. That’s why DLRS is such wonderful tool. I just hate it that it has to be like this. Or does it?&lt;/p&gt;

&lt;h2 id=&quot;soql-to-the-rescue&quot;&gt;SOQL to the rescue&lt;/h2&gt;

&lt;p&gt;What infuriated me for years was that SOQL can actually do this for you with aggregate queries. For example, if you captured your donations for an Account in a child object called Donations, you could do the following with SOQL:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;SELECT Account__c, SUM(Amount__c) FROM Donation__c GROUP BY Account__c HAVING SUM(Amount__c) &amp;gt; 1000
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;But the goal here is to make this reportable. Sure, you might get away with having devs and admins run these SOQL queries for the business, but part of scalability is providing a mechanism for self-service. So you make the rollup field.&lt;/p&gt;

&lt;h2 id=&quot;enter-the-apex-connector-framework&quot;&gt;Enter the Apex Connector Framework&lt;/h2&gt;

&lt;p&gt;I was playing with the &lt;a href=&quot;https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_connector_start.htm&quot;&gt;Apex Connector Framework&lt;/a&gt; to pull some data into Salesforce from an API so that they could be reportable. The flow of this framework is as follows&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Define fields on an external object (i.e. how will this object be represented in Salesforce?)&lt;/li&gt;
  &lt;li&gt;Extend the framework classes, which mainly allow you to interpret a SOQL query, sent the search criteria to an API with a callout&lt;/li&gt;
  &lt;li&gt;Parse the results and map to rows for the external object.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After you do this, the external object becomes reportable! So I thought, what if instead of doing a callout we just did the aggregate query from above and mapped those results to the external object. In other words, I have an external object called &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;AccountRollup__x&lt;/code&gt; with a field &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;TotalDonations__c&lt;/code&gt;, and every time it is queried, it does an aggregate of Account records?&lt;/p&gt;

&lt;p&gt;Turns out, it kind of works and it’s reportable! You can run a report using filters on the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;TotalDonations&lt;/code&gt; field as if you were reporting on a regular rollup field.&lt;/p&gt;

&lt;p&gt;See this repo for code samples of a prototype that does just this you can try in a scratch org: &lt;a href=&quot;https://github.com/seanpat09/live-rollups&quot;&gt;https://github.com/seanpat09/live-rollups&lt;/a&gt;&lt;/p&gt;

&lt;h2 id=&quot;caveats&quot;&gt;Caveats&lt;/h2&gt;
&lt;p&gt;I ran into a few issues while building this prototype.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;For some reason, I could not display fields related Account fields on the report as I was getting a system error with a gack. That’s why I suggest linking to the summary field in the report with the Name column, which can give you access to the Account via the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;AccountRecord&lt;/code&gt; lookup field&lt;/li&gt;
  &lt;li&gt;When I tried to make the rollup summary record a parent to the Account, the report would just break, not allowing me to do the filtering at all&lt;/li&gt;
  &lt;li&gt;You’re limited to whatever SOQL limits you are normally limited to, so if you have a really high volume org, this might not work at all.&lt;/li&gt;
  &lt;li&gt;I’m not sure of the licensing on the Apex Connector Framework, but I was able to use it in an Enterprise org that does NOT have the license for Salesforce Connect.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So what does this mean? I’m not really sure yet to be honest. But if this works and scales scales, it might be another option to rollup summaries without having to run background jobs to maintain the data! Instead of debugging a job, you’re debugging SOQL queries instead, which might be much easier to maintain for your team.&lt;/p&gt;

</description>
        <pubDate>Mon, 23 May 2022 08:00:00 +0000</pubDate>
        <link>http://seanpat09.github.io/2022/05/23/a-new-way-to-do-rollups.html</link>
        <guid isPermaLink="true">http://seanpat09.github.io/2022/05/23/a-new-way-to-do-rollups.html</guid>
        
        <category>rollup-summary</category>
        
        <category>apex</category>
        
        
      </item>
    
      <item>
        <title>Design Patterns in Salesforce: Abstract Factory</title>
        <description>&lt;p&gt;&lt;em&gt;Design patterns are powerful tool to build reusuable code, but they are hard to implement in without examples and use cases. This is a series that follows the book Design Patterns: Elements of Resuable Object-Oriented Software a provides examples that relate to Salesforce&lt;/em&gt;&lt;/p&gt;

&lt;h2 id=&quot;abstract-factory&quot;&gt;Abstract Factory&lt;/h2&gt;

&lt;p&gt;The Abstract Factory design pattern is useful for creating related objects that follow an interface. I find this pattern particularly useful for building resuable Lightning Web Components. Let’s use this pattern to build a resuable custom related list.&lt;/p&gt;

&lt;h2 id=&quot;use-case-viewing-grandchild-records-on-a-detail-page&quot;&gt;Use Case: Viewing grandchild records on a detail page.&lt;/h2&gt;

&lt;p&gt;Out of the box related lists in Salesforce are great for displaying child records on a detail page, but what if you wanted to display all the grand child records? Imagine a national charity that oversees local branches. Each branch is represented as an Account record, and the volunteers for that branch are represented as Contact records. When a volunteer is onboarded, there are several onboarding tasks required before they can start any work, such as a background check, filling out emergency contact info, and creating a user in their Salesforce Community.&lt;/p&gt;

&lt;p&gt;Region managers oversee local branches in their assigned region, so in addition to other information on the Account, the Region Managers want to see all the outstanding tasks for all the Contacts on the Account detail page in a table (for the sake of this post let’s pretend this is the best solution).&lt;/p&gt;

&lt;h2 id=&quot;a-non-resuauble-approach&quot;&gt;A non-resuauble approach&lt;/h2&gt;

&lt;p&gt;If you didn’t care about resuability (which is ok if you don’t have that bandwidth!), you could build an LWC like this (I can’t guarantee this compiles)&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;//contactTasks.html --&amp;gt;

&amp;lt;template&amp;gt;
  &amp;lt;lightning-datatable
    key-field=&quot;id&quot;
    data={data}
    columns={columns}&amp;gt;
  &amp;lt;/lightning-datatable&amp;gt;
&amp;lt;/template&amp;gt;

//contactTasks.js

import { LightningElement } from 'lwc';
import getTasks from '@salesforce/apex/ContactTasksController.getTasks';

const columns = [
  { label: 'Subject', fieldName: 'Subject' },
  { label: 'Assigned To', fieldName: 'WhoId'}
];

export default class ContactTasks extends LightningElement {
  data = [];
  columns = columns;
  recordId;

  async connectedCallback() {
      const data = await getTasks({ accountId: this.recordId });
      this.data = data;
  }
}

//ContactTasksController

public class ContactTasksController {
  @AuraEnabled
  public static List&amp;lt;Task&amp;gt; getTasks(Id accountId) {
    List&amp;lt;Task&amp;gt; allTasks = new List&amp;lt;Task&amp;gt;();
    for(Contact aContact : [SELECT (SELECT Subject, WhoId FROM Tasks) FROM Contact WHERE AccountId = :accountId]) {
      allTasks.addAll(aContact.Tasks);
    }

    return allTasks;
  }
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;A solution like this is fine, but in the future you might get a similar request where you want to display all tasks for all the Contacts related to an Opportunity. Or maybe just some other data. You could create a separate LWC with a separate Apex controller, but you might notice that the LWC isn’t doing all that much, just displaying data. Most of the differences is in building the columns and getting the data. Wouldn’t it be nice to &lt;em&gt;abstract&lt;/em&gt; out the logic that builds the table data? Like some kind of &lt;em&gt;abstract factory&lt;/em&gt;?&lt;/p&gt;

&lt;h2 id=&quot;the-abstract-factory-pattern&quot;&gt;The Abstract Factory Pattern&lt;/h2&gt;

&lt;p&gt;The abstract factory pattern can be broken down into 3 parts:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;A interface that specifies what an implementation must be able to build.&lt;/li&gt;
  &lt;li&gt;A consumer that can request a specific implementation, but can handle any implementation of the interface&lt;/li&gt;
  &lt;li&gt;Concrete implementations that fulfill the interface&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So in this case we need an interface that can build the columns and rows for a table, a component that can specify which columns and tables to display and actually display them, and then 1 or more concrete implementations with the business logic. How would this look?&lt;/p&gt;

&lt;p&gt;First let’s create some objects to define what we’re building:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;public class CustomListColumn {
  public String label;
  public String fieldName;

  public CustomListColumn(String label, Object fieldName) {
    this.label = label;
    this.fieldName = fieldName;
  }
}

public class CustomListRow {
  public Object value;

  public CustomListRow(Object value) {
    this.value = value;
  }
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Next our interface for our factory.&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;public interface ICustomListFactory {
  List&amp;lt;CustomListColumn&amp;gt; buildColumns();
  List&amp;lt;CustomListRow&amp;gt; buildRows(Id parentId);
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;And then our concrete implementation of that factory&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;public class AccountContactsTaskListFactory implements ICustomListFactory{
  public List&amp;lt;CustomListColumn&amp;gt; buildColumns() {
    List&amp;lt;CustomListColumn&amp;gt; columns = new List&amp;lt;CustomListColumn&amp;gt;();
    columns.add(
      new CustomListColumn('Subject', 'Subject'),
      new CustomListColumn('Assigned To', 'WhoId')
    );
  }
  public List&amp;lt;CustomListRow&amp;gt; buildRows(Id parentId) {
    List&amp;lt;CustomListRow&amp;gt; allTasks = new List&amp;lt;CustomListRow&amp;gt;();
    for(Contact aContact : [SELECT (SELECT Subject, WhoId FROM Tasks) FROM Contact WHERE AccountId = :parentId]) {
      for(Task aTask: aContact.Tasks) {
        allTasks.add(new CustomListRow(aTask));
      }
    }
    return allTasks;
  }
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Next let’s make our component more generalized. It now gets the columns in addition to the rows from the Apex controller. It also passes a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;listType&lt;/code&gt; parameter to specify which implementation should be used&lt;/p&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;//customRelatedList.html --&amp;gt;

&amp;lt;template&amp;gt;
  &amp;lt;lightning-datatable
    key-field=&quot;id&quot;
    data={data}
    columns={columns}&amp;gt;
  &amp;lt;/lightning-datatable&amp;gt;
&amp;lt;/template&amp;gt;

//contactTasks.js

import { LightningElement } from 'lwc';
import getTableData from '@salesforce/apex/CustomRelatedListController.getTableData';

export default class CustomRelatedList extends LightningElement {
  data = [];
  columns = columns;
  recordId;

  @api
  listType

  async connectedCallback() {
      const tableData = await getTableData({ recordId: this.recordId, listType: this.listType });
      this.data = tableData.rows;
      this.columns = tableData.columns;
  }
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Finally we create a controller that will handle getting the correct implementation based on the listType passed from the LWC&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;public class CustomRelatedListController {
  @AuraEnabled
  public TableData getTableData(Id recordId, String listType) {
    ICustomListFactory factory = getCustomListFactory(listType);
    
    TableData data = new TableData();
    data.rows = factory.getRows(recordId);
    data.columns = factory.getColumns();

    return data;
  }

  private ICustomListFactory getCustomListFactory(String listType) {
    if(listType === 'AccountContactsTasks') {
      return new AccountContactsTaskListFactory();
    } else {
      throw CustomRelatedListControllerException('List Type: ' + listType + ' not supported');
    }
  }


  public TableData {
    @AuraEnabled
    List&amp;lt;CustomListColumn&amp;gt; columns;
    List&amp;lt;CustomListRow&amp;gt; rows;
  }

  public CustomRelatedListControllerException extends Exception {}
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;benefits&quot;&gt;Benefits&lt;/h2&gt;

&lt;p&gt;This seems like a lot of extra code and for just a single use case it probably is. The value comes with its reuse. Let’s add another table, this time displaying Contact information related to an Opportunity. Specifically, the Contact’s full name, parent account, grandparent Account (i.e. up an additional Account level in the hierarchy), and the Account website.&lt;/p&gt;

&lt;p&gt;Here’s the concrete implementation:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;public class OpportunityContactListFactory implements ICustomListFactory{
  public List&amp;lt;CustomListColumn&amp;gt; buildColumns() {
    List&amp;lt;CustomListColumn&amp;gt; columns = new List&amp;lt;CustomListColumn&amp;gt;();
    columns.add(
      new CustomListColumn('Full Name', 'fullName'),
      new CustomListColumn('Parent Account', 'parentAccount'),
      new CustomListColumn('Grandparent Account', 'grandParentAccount'),
      new CustomListColumn('Website', 'website')
    );
  }
  public List&amp;lt;CustomListRow&amp;gt; buildRows(Id parentId) {
    List&amp;lt;CustomListRow&amp;gt; contacts = new List&amp;lt;CustomListRow&amp;gt;();
    for(OpportunityContactRole ocr :
      [ SELECT Contact.Fullname,
               Contact.Account.Name,
               Contact.Account.Account.Name,
               Contact.Account.Website
        FROM OpportunityContactRole WHERE OpportunitId = :parentId]
    ) {
        Map&amp;lt;String, String&amp;gt; contactRow = new Map&amp;lt;String, String&amp;gt;{
          'fullName' =&amp;gt; ocr.Contact.FullName,
          'parentAccount' =&amp;gt; ocr.Contact.Account.Name,
          'grandParentAccount' =&amp;gt; ocr.Contact.Account.Account.Name,
          'website' =&amp;gt; ocr.Contact.Account.Website,
        }
        contacts.add(contactRow);
    }
    return contacts;
  }
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;And then we update the controller to handle this new list type:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;  private ICustomListFactory getCustomListFactory(String listType) {
    if(listType === 'AccountContactsTasks') {
      return new AccountContactsTaskListFactory();
    } else if(listType === 'OpportunityContacts') {
      return new OpportunityContactListFactory();
    } else {
      throw CustomRelatedListControllerException('List Type: ' + listType + ' not supported');
    }
  }
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;And now our LWC can handle a different use case given a different &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;listType&lt;/code&gt; API value, without having to change any of its code! Arguably the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;getCustomListFactory&lt;/code&gt; method should be pulled into its own class so that the controller also does not have to be updated either.&lt;/p&gt;

&lt;p&gt;I find myself using this pattern often with LWCs to take advantage of the reusability of components. It can take a little extra upfront work, but can save you a lot of time in the long run in terms of code you don’t have to write and also for creating small testable components.&lt;/p&gt;
</description>
        <pubDate>Thu, 12 May 2022 08:00:00 +0000</pubDate>
        <link>http://seanpat09.github.io/2022/05/12/salesforce-design-patterns-abstract-factory.html</link>
        <guid isPermaLink="true">http://seanpat09.github.io/2022/05/12/salesforce-design-patterns-abstract-factory.html</guid>
        
        <category>design patterns</category>
        
        <category>apex</category>
        
        <category>abstract factory</category>
        
        
      </item>
    
      <item>
        <title>Pull Requests are an Essential Part of Security</title>
        <description>&lt;h2 id=&quot;separation-of-duties&quot;&gt;Separation of Duties&lt;/h2&gt;

&lt;p&gt;Before I became a software engineer, I studied accounting and even worked as an auditor for a brief period. Not much accounting knowledge remains, but a few nuggets stayed with me, particularly the internal control known as “Separation of Duties” (not to be confused with the design principle “Separation of Concerns”). To summarize, no one person should have complete control over an entire transaction. For example, the person collecting cash from a customer for a bill should not be the one to also record the transaction and deposit the cash. Otherwise, that person could collect funds, credit the customer’s account, and take a little off the top before making the bank deposit. Having another person in this process does not eliminate the risk, but colluding with someone else is harder to hide. You know what they say about the only way for two people keep a secret.&lt;/p&gt;

&lt;p&gt;What does this have to with code and pull requests? Pull requests serve a similar security purpose. Not only do pull requests provide the nice to haves of allowing for asynchronous collaboration, a second set of eyes, etc, they are also a very important auditing artifact and should be considered an essential component of your delivery process.&lt;/p&gt;

&lt;p&gt;Code controls business logic, often at a pace magnitudes faster than what any human can keep up with. And because of the complex nature of writing code, it poses a huge security vulnerability where malicious behavior can easily be obfuscated. For example a &lt;a href=&quot;https://en.wikipedia.org/wiki/Fork_bomb&quot;&gt;fork bomb&lt;/a&gt; looks like an inconspicuous line of code, but can actually crash your entire system. Because of which, every line of code that makes its way into any system of importance should be reviewed by someone else. Pull requests not only provide the mechanism to do this review, but also create the audit trail for every change.&lt;/p&gt;

&lt;h2 id=&quot;components-of-a-pull-request&quot;&gt;Components of a Pull Request&lt;/h2&gt;
&lt;p&gt;Let’s examine the components of a pull requests and how they contribute to security.&lt;/p&gt;

&lt;p&gt;First and foremost a pull request aggregates proposed changes via its commits. By using &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;git blame&lt;/code&gt; you are able to find the latest commit attributed to every line in the codebase. With this you are able to follow the history of any line change. If you ever wondered why a change was made, following the commit history is a great start. With GitHub, you can even see which pull request was the one that brought that commit into the current branch.&lt;/p&gt;

&lt;p&gt;Not only can you see the history of changes, but the commits attribute the changes to the person that introduced it. Commit history great, but also being able to talk to the person that made the change can help provide more context that’s not in the code.&lt;/p&gt;

&lt;p&gt;So far these are just things you get from a commit history alone. Pull requests show their value in the additional layer of tracking they provide. With a pull request, we can see the reviewer that approved the merge. Now you have at least two people involved in the process of getting code into production (provided that you have configured your version control to not allow self-approvals).&lt;/p&gt;

&lt;p&gt;The pull request itself also provides an area to have a discussion - on the features, the code decisions, tech debt introduction. By doing this in the pull request, this discussion is linked very closely to the code itself. You could have the discussion happen in some other document, but several months down the line that document may get lost to some obscure folder in your company’s google drive.&lt;/p&gt;

&lt;p&gt;For extra credit, you could also tag pull requests with the work item associated to the pull request, so you know why a change was made.&lt;/p&gt;

&lt;h2 id=&quot;security-benefits-and-beyond&quot;&gt;Security Benefits and Beyond&lt;/h2&gt;
&lt;p&gt;With all of these ingredients together, you can trace from a single line change who made the change, approved the change, approved the item to be worked on at all, and any context capture in any comments for the PR! And with all theses duties separated by person, you’ve mitigated a lot of the risk of any one person trying to act maliciously within an organization.&lt;/p&gt;

&lt;p&gt;As an auditor, this kind of paper trail is gold. In fact, I’ve been involved in security audits that went super smoothly because this process was in place. In addition to security benefits, it was so useful to be able to see the context of something far into the future. Have you ever looked at code that YOU wrote and wondered why you took some odd approach? With this trail you can find the context of the original change, hopefully with an explanation of why.&lt;/p&gt;

&lt;h2 id=&quot;constructive-review-vs-fodder-for-punishment&quot;&gt;Constructive Review vs. Fodder for Punishment&lt;/h2&gt;
&lt;p&gt;I want to emphasize that this internal control is not about placing blame. Accountability is important, but if this process is wielded as a threat, your team will be reluctant to to adhere to it. Should something happen, a bug, security breach, or even just a missed feature, this process should be used to trace the source of the problem. Afterwards a constructive, blameless discussion can happen where processes can improved. What happened in the past happened - the only thing to do is move on and learn from it. If leadership uses this process to punish contributors for their mistakes, the engineers will find ways to not be associated to the changes. Like any tool, its value to the organization highly depends on the culture those using it.&lt;/p&gt;

&lt;p&gt;There are not many things that I consider essential for an engineering org, but pull requests are something that is a must have for me, and possibly a deal breaker for me if an organization refused to use them without a much better alternative. Even when I was the only working on a project professionally, I STILL used pull requests so that I could have the benefits of the audit trail, both for my future self or anyone else who would have to inherit my work after I left.&lt;/p&gt;

</description>
        <pubDate>Wed, 16 Feb 2022 08:00:00 +0000</pubDate>
        <link>http://seanpat09.github.io/2022/02/16/why-prs-are-essential.html</link>
        <guid isPermaLink="true">http://seanpat09.github.io/2022/02/16/why-prs-are-essential.html</guid>
        
        <category>work life balance</category>
        
        
      </item>
    
      <item>
        <title>Good morning, and in case I don't see ya: Good afternoon, good evening, and good night!</title>
        <description>&lt;p&gt;Throughout my career I have joined and left several organizations, all for different reasons, and I have always felt confident in my choice to move on. For the first time, however, I am as equally excited in my next role as I am saddened to leave my previous one. Later this month, I will be joining the team at Twitch, leaving my current role at Salesforce.&lt;/p&gt;

&lt;p&gt;I cannot express enough how lucky I was to be able to work with the people that I did at Salesforce. These were some of the most capable individuals I have ever worked with, and I found myself continually impressed by everyone. The way I was able to thrive, grow, and learn was phenomenal. Not just in the technical skills that I have gained, but also the numerous lessons I have learned in what we typically call “soft” skills. Lessons on how to lead and be a part of phenomenal teams, how to work productively and, more importantly, sustainably. What it means to work together with compassion and empathy. And what it truly means to work in a way that benefits not just the company, but myself.&lt;/p&gt;

&lt;p&gt;There are too many people to thank in the organization. I am a different person, changed for the better, and for good, because of everyone I had the pleasure to interact with, and I hope our friendships will continue.&lt;/p&gt;

&lt;p&gt;In particular I wanted to thank a few individuals in leadership that were instrumental in my growth. To Joysorlyn Dixon, Nina Patel, and Alex Peralta, the leaders that I was fortunate enough to work with closely on a daily basis, thank you for your mentorship, your guidance, and your friendship. I have never felt so valued and supported - even when I decided to move on to a new role you still put my wellbeing and my growth first. Whether I cried ugly tears on camera, vented my frustration at a tricky code base or situation, or just needed some advice on how to lead, your support was key to my success and ability to keep moving forward.&lt;/p&gt;

&lt;p&gt;The attitude of servant leadership is something that is rare and a treasure - to not only help guide the success of the product, but also prioritize the growth of the individual is so admirable. I hope I can take those lessons with me and pass them on.&lt;/p&gt;

&lt;p&gt;I am so lucky to have had this opportunity for the last few years to work with such great people. To my leadership, to my teammates on my scrum teams, and to those I was not fortunate enough to collaborate with more extensively - the words “thank you” are not enough to express my gratitude for all of your patience, encouragement, and support.&lt;/p&gt;

&lt;p&gt;And so onwards to new adventures! I am so excited and grateful for this next opportunity and only hope to be even a fraction as impactful to those I work with in the future as everyone I worked with here at Salesforce has been for me.&lt;/p&gt;
</description>
        <pubDate>Tue, 03 Aug 2021 07:00:00 +0000</pubDate>
        <link>http://seanpat09.github.io/2021/08/03/git-checkout-b.html</link>
        <guid isPermaLink="true">http://seanpat09.github.io/2021/08/03/git-checkout-b.html</guid>
        
        <category>work life balance</category>
        
        
      </item>
    
      <item>
        <title>Toxic Positivity - It's OK to Not Be OK</title>
        <description>&lt;p&gt;A friend recently introduced me to the idea of “toxic positivity”, which can be summarized as the “everything is awesome!” approach to life. I can’t speak to other industries, but I see this attitude all too often in tech. While I believe we should encourage some level of positive thinking, when we don’t allow ourselves the space for our pain, for our grief, we tip the balance into toxicity.&lt;/p&gt;

&lt;h2 id=&quot;its-not-me-its-you&quot;&gt;It’s not me, it’s you&lt;/h2&gt;

&lt;p&gt;If you work in tech, you have probably run into one of the following mantras at some point:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;em&gt;Celebrate your failures&lt;/em&gt;&lt;/li&gt;
  &lt;li&gt;&lt;em&gt;Don’t come to me with problems, come to me solutions&lt;/em&gt;&lt;/li&gt;
  &lt;li&gt;&lt;em&gt;Happiness is a choice&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When something bad happens, it’s actually good! When you have a problem, it’s not worth talking to me about it unless you know how to fix it. You can be happy too - just &lt;em&gt;be&lt;/em&gt; happy.&lt;/p&gt;

&lt;p&gt;There are plenty more, but it all boils down to the idea that if you are not satisfied with a situation, you have the power to fix it. In the book “America the Anxious” by Ruth Whippman, she summarizes the problem with this mentality beautifully:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;But it doesn’t take long to realize that the flip side of this logic is that, if I am not happy, then it is All My Own Fault&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Let’s be honest here - failure sucks and I do not want to dwell on it, much less celebrate it. Sometimes I have a problem, I have no idea what to do, and I cannot face it alone. Sometimes I am sad, angry, disgusted, scared, and happiness won’t help me right now, because I need to process this feeling.&lt;/p&gt;

&lt;p&gt;In my career, especially in the last year, I cannot stress enough how lucky I am to have worked in environments (some more equipped than others) where I have been given the space to have these feelings, to step away, to reset. While this is a practice I want to encourage, it still implies that the underlying problem is within the individual. People may be given the time to get away, but what happens when the problem is rooted in the way we work?&lt;/p&gt;

&lt;h2 id=&quot;everything-is-awesome&quot;&gt;Everything is awesome!&lt;/h2&gt;

&lt;p&gt;I think retros are the most important thing you can do as team. Taking the time to reflect on the work you just completed, to celebrate the successes, to re-examine the challenges, is necessary for the continual improvement of the team. Too often, however, I’ve seen retro devolve into just being a “good vibes sesh”, a full hour of teammates just giving each other kudos. Recognizing and celebrating each other is important part of bonding the team, but it can’t be the only point of the discussion.&lt;/p&gt;

&lt;p&gt;If retro doesn’t allow people to recognize the things that are not working, when will the team ever get the chance to fix it? Even worse, people may be actively discouraged from bringing up anything that’s going wrong because no one wants to be the person that “kills the vibes.” I have attended retros for sprints that completely burned me out, just seething in silence as the team celebrated all the “success.” I don’t care if the release went out on time, should we really be patting ourselves on the back for the all nighter we just pulled to make it so? Shouldn’t we instead be examining the circumstances that led to this situation in the first place? But I hesitated to speak up because I didn’t want to rain on the parade.&lt;/p&gt;

&lt;p&gt;Spinning things as a positive does not magically make things positive. Instead, it isolates each individual who is struggling with the status quo, who actually may not be alone in their thinking! It gaslights them into thinking that maybe the process is fine and that &lt;em&gt;they&lt;/em&gt; are problem. After all, no one has said anything, so it must be fine, right? And so what happens? They leave.&lt;/p&gt;

&lt;h2 id=&quot;bringing-an-attitude-of-balance&quot;&gt;Bringing an attitude of balance&lt;/h2&gt;

&lt;p&gt;I am by no means encouraging complain-a-thons. The key here is balance. Here are a few ways you could help bring that balance to your team:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;During retros, time-box each part so that there is space to discuss both the successes and the challenges&lt;/li&gt;
  &lt;li&gt;Be the person that brings up challenges. Try to do so calmly, without attacking, and using neutral words to keep things constructive. You are probably not the only person that is feeling that way.&lt;/li&gt;
  &lt;li&gt;If you’re a leader, recognize when things are going great and when things are not going well. And provide the space for the team to try things to remedy the problem. Yes, it might slow things down, but I can promise you that things will come to a screeching halt when your team decides to leave instead.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We have to recognize that in order to improve, we have to come to terms that the things we feel can’t just be ignored, that the things that aren’t going well need to be addressed. Recognizing that things can be improved doesn’t mean that everything is currently broken. But how will things get better if we don’t see them for how they truly are?&lt;/p&gt;
</description>
        <pubDate>Tue, 27 Jul 2021 07:00:00 +0000</pubDate>
        <link>http://seanpat09.github.io/2021/07/27/toxic-positivity.html</link>
        <guid isPermaLink="true">http://seanpat09.github.io/2021/07/27/toxic-positivity.html</guid>
        
        <category>work life balance</category>
        
        
      </item>
    
      <item>
        <title>Merging DOCX files natively in Apex.</title>
        <description>&lt;p&gt;When it comes to document generation in native Salesforce, I always assumed that your only options were something really simple like using &lt;a href=&quot;https://developer.salesforce.com/docs/atlas.en-us.apexref.meta/apexref/apex_methods_system_blob.htm&quot;&gt;Blob.toPDF&lt;/a&gt; or rendering a Visualforce page as a PDF and saving it as a file. Neither of there were very pragmatic - the Blob method left much to be desired in terms of formatting and the Visualforce approach was too hard to maintain. As such, when someone would ask for document generation I would just point them to an ISV like Conga or Drawloop. Surely working with a Word document wouldn’t be possible in Apex, right?&lt;/p&gt;

&lt;h2 id=&quot;the-docx-file-format&quot;&gt;The DOCX file format.&lt;/h2&gt;

&lt;p&gt;I always assumed that a Word document was a complex file format, inscrutable to the likes of me. For a long time that was true - the original .doc file extension was a more proprietary format binary-based format. In the early 2000s, in response to open source solutions like Open Office, Microsoft created the DOCX file format that used XML.&lt;/p&gt;

&lt;p&gt;In fact, a DOCX file is actually just a zipped file of various xml documents. You can try it right now - go to Google docs, type some stuff in it, click File &amp;gt;Download &amp;gt; Microsoft Doc (.docx). Unzip that file and in the resulting folder you can see the xml files yourself. The file we’ll focus on is is named &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;document.xml&lt;/code&gt;.&lt;/p&gt;

&lt;h2 id=&quot;working-with-documentxml&quot;&gt;Working with document.xml&lt;/h2&gt;

&lt;p&gt;I created a Word document that looked something like this, but formatted:&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;%%title%%&lt;/p&gt;

&lt;p&gt;%%header1%%&lt;/p&gt;

&lt;p&gt;%%header2%%&lt;/p&gt;

&lt;p&gt;%%content%%&lt;/p&gt;
&lt;hr /&gt;

&lt;p&gt;I unzipped that file and took a look at the document.xml file, searched from my merge fields, and they were relatively to find. For example, the “content” merge field was displayed like this:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&amp;lt;w:t xml:space=&quot;preserve&quot;&amp;gt;%%contentText%%&amp;lt;/w:t&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Seeing that, I realized I could just do a find and replace on these merge fields, re-zip these files as a new archive and &lt;em&gt;tada&lt;/em&gt;, basic document generation. And &lt;em&gt;tada&lt;/em&gt; indeed it does work like that. Could I do this programmatically?&lt;/p&gt;

&lt;h2 id=&quot;document-generation-with-apex&quot;&gt;Document generation with Apex,&lt;/h2&gt;
&lt;p&gt;Reading an XML file in Apex is simple enough - just get the blob from a ContentVersion or an Attachment record and turn it into a string. But how do I get that document.xml file out of the docx file?&lt;/p&gt;

&lt;p&gt;Unfortunately, there does not appear to be a native way to extract files from a zip file in Apex. Luckily someone has solved this problem for us. Enter &lt;a href=&quot;https://github.com/pdalcol/Zippex&quot;&gt;Zippex&lt;/a&gt;, a native Apex Zip utility for the salesforce.com platform. I installed this into a scratch org, uploaded my .docx file, and started to break down the problem:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Unzip the docx file&lt;/li&gt;
  &lt;li&gt;Get the document.xml file&lt;/li&gt;
  &lt;li&gt;Do a find and replace on the merge fields&lt;/li&gt;
  &lt;li&gt;Rezip the file and save the changes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I first started by just hard coding IDs to make this a little easier so I didn’t have to worry about querying for ContentDocuments:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;ContentVersion cv = [
    SELECT VersionData
    FROM ContentVersion
    WHERE ContentDocumentId = 'SOME_CONTENT_DOCUMENT_ID'
    AND IsLatest = true
];

Zippex sampleZip = new Zippex(cv.VersionData);
Blob fileData = sampleZip.getFile('word/document.xml');
String docxml = fileData.toString();

docxml = docxml.replace(
  '%%title%%', 'Merged Title!',
  '%%header1%%', 'Merged Header 1!',
  '%%header2%%', 'Merged Header 2!',
  '%%content%%', 'Merged Content!',
);

Blob mergedData = Blob.valueof(docxml);
sampleZip.addFile('word/document.xml', mergedData, null);

String newTitle = 'merged' + Datetime.now().getTime();
ContentVersion mergedFile = new ContentVersion();
mergedFile.VersionData = sampleZip.getZipArchive();
mergedFile.Title = newTitle;
mergedFile.ContentLocation= 's';
mergedFile.PathOnClient= newTitle+'.docx';
insert mergedFile;

Id conDoc = [SELECT ContentDocumentId FROM ContentVersion WHERE Id =:mergedFile.Id].ContentDocumentId;
ContentDocumentLink cdl = new ContentDocumentLink();
cdl.ContentDocumentId = conDoc;
cdl.LinkedEntityId = 'SOME_PARENT_RECORD_ID';
cdl.ShareType = 'I';
cdl.Visibility = 'AllUsers';
insert cdl;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;After I ran that, on my parent account record that I had put my docx attachment, I had a new docx file that had the merge fields populated!&lt;/p&gt;

&lt;figure&gt;
  &lt;img src=&quot;http://seanpat09.github.io/assets/img/apex-doc-gen-hulk.jpg&quot; alt=&quot;Hulk saying 'Native Apex document generation&quot; /&gt;
&lt;/figure&gt;

&lt;p&gt;With a proof of concept I reworked the code a little bit so that it’s a little more reusable. &lt;a href=&quot;https://github.com/seanpat09/docx-merger&quot;&gt;Click here for the github repo&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;With that code you can do something like this to generate documents with merge fields:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;//This File must be a docx file
Id contentDocumentId = '&amp;lt;REPLACE_WITH_DOC_ID&amp;gt;';
Id parentId = '&amp;lt;REPLACE_WITH_PARENT_ID&amp;gt;';
Map&amp;lt;String,String&amp;gt; mergeFields = new Map&amp;lt;String,String&amp;gt;{
        '%%mergefield%%' =&amp;gt;  'I HAVE BEEN MERGED',
        '%%header1%%' =&amp;gt;  'NEW HEADER 1',
        '%%header2%%' =&amp;gt;  'NEW HEADER 2'
    };
new DocxMerger()
    .mergeDoc(contentDocumentId, parentId, mergeFields);
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;too-good-to-be-true&quot;&gt;Too good to be true?&lt;/h2&gt;

&lt;p&gt;The document that I merged was tiny, but when I ran the code it took a while. The resulting debug log was 18 megabytes, so this seems to be an expensive operation. So I have no idea what would happen if I tried to merge a larger, more complex document. My hope is that the bottleneck is in the unzipping part itself and if that’s the case I can potentially try to do this client side using LWC. Of course, however, that would mean that you can’t bulkify this process via something like a batch job.&lt;/p&gt;

&lt;p&gt;I’m actually quite surprised I got this far - usually I give up on these experiments but this wasn’t actually too hard. I’ll need to figure out where this project goes from here. Do I add more features, or does this end up in the pile of forgotten repos in GitHub? Either way, it was a cool experiment!&lt;/p&gt;
</description>
        <pubDate>Tue, 22 Jun 2021 07:00:00 +0000</pubDate>
        <link>http://seanpat09.github.io/2021/06/22/merging-documents-in-apex.html</link>
        <guid isPermaLink="true">http://seanpat09.github.io/2021/06/22/merging-documents-in-apex.html</guid>
        
        <category>document generation</category>
        
        <category>apex</category>
        
        
      </item>
    
  </channel>
</rss>
