<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>Dan Schultzer</title>
  <link href="https://danschultzer.com/" rel="alternate" type="text/html" />
  <link href="https://danschultzer.com/atom.xml" rel="self" type="application/atom+xml" />
  <updated>2025-02-15T00:00:00Z</updated>
  <author>
    <name>Dan Schultzer</name>
  </author>
  <id>https://danschultzer.com/</id>

  
  <entry>
    <title>ECharts in Phoenix LiveView</title>
    <link href="https://danschultzer.com/posts/echarts-phoenix-liveview" />
    <id>https://danschultzer.com/posts/echarts-phoenix-liveview</id>
    <updated>2025-02-15T00:00:00Z</updated>
    <summary>How to add live-updating charts with ECharts in Phoenix LiveView</summary>
    <content type="html">
      <![CDATA[
        <p>
To display charts in Phoenix, we can either generate them server-side (e.g., using <a href="https://hex.pm/packages/contex"><code class="inline">Contex</code></a>) or client-side with JavaScript. I would prefer a server-side solution with pure SVG/CSS, but currently, nothing beats JavaScript charting libraries. However, we can still have the backend do most of the work and live update the charts with a minimal JS hook.</p>
<p>
In this post, I’ll go over how to implement <a href="https://echarts.apache.org/">ECharts</a> in a Phoenix LiveView app with live updates.</p>
<p>
  <img src="/images/posts/2025/02-11-echarts-phoenix-liveview.open-graph-graphs.gif" alt="ECharts in PhoenixLiveView">
</p>
<h2>
Set up ECharts</h2>
<p>
Add <a href="https://echarts.apache.org/en/download.html"><code class="inline">echarts.min.js</code></a> to your <code class="inline">assets/vendor</code> directory, and set up <code class="inline">assets/js/hooks.js</code> to render charts on events pushed from the backend:</p>
<pre><code class="javascript">// assets/js/hooks.js
let Hooks = {};

import * as echarts from &quot;../vendor/echarts.min&quot;

Hooks.Chart = {
  render(chart, option) {
    // The legend selection should not be overridden with subsequent updates
    if (chart.getOption() &amp;&amp; option.legend &amp;&amp; option.legend.selected) {
      delete option.legend.selected
    }

    chart.setOption(option)
  },

  mounted() {
    let chart = echarts.init(this.el)

    this.handleEvent(`chart-option-${this.el.id}`, (option) =&gt;
      this.render(chart, option)
    )
  }
}

export default Hooks;</code></pre>
<p>
Initialize the LiveSocket in <code class="inline">assets/js/app.js</code> with the hooks:</p>
<pre><code class="javascript">// assets/js/app.js

// ...

import &quot;phoenix_html&quot;;
// Establish Phoenix Socket and LiveView configuration.
import { Socket } from &quot;phoenix&quot;
import { LiveSocket } from &quot;phoenix_live_view&quot;
import topbar from &quot;../vendor/topbar&quot;
import Hooks from &quot;./hooks&quot; // 1. Import Hooks

let csrfToken = document.querySelector(&quot;meta[name=&#39;csrf-token&#39;]&quot;).getAttribute(&quot;content&quot;);
let liveSocket = new LiveSocket(&quot;/live&quot;, Socket, {
  longPollFallbackMs: 2500,
  params: { _csrf_token: csrfToken },
  hooks: Hooks // 2. Add it to the liveSocket
});

// ...</code></pre>
<p>
This is all the JavaScript we need. Our backend will push the chart options used to render the graphs.</p>
<p>
Implement some dummy data that we want to chart:</p>
<pre><code class="makeup elixir"><span class="c1"># lib/my_app/data.ex</span><span class="w">
</span><span class="kd">defmodule</span><span class="w"> </span><span class="nc">MyApp.Data</span><span class="w"> </span><span class="k" data-group-id="5619018071-1">do</span><span class="w">
  </span><span class="kd">def</span><span class="w"> </span><span class="nf">get_line_stack_data</span><span class="p" data-group-id="5619018071-2">(</span><span class="n">cumulative</span><span class="w"> </span><span class="o">\\</span><span class="w"> </span><span class="no">nil</span><span class="p" data-group-id="5619018071-2">)</span><span class="w"> </span><span class="k" data-group-id="5619018071-3">do</span><span class="w">
    </span><span class="p" data-group-id="5619018071-4">{</span><span class="w">
      </span><span class="p" data-group-id="5619018071-5">[</span><span class="s">&quot;Mon&quot;</span><span class="p">,</span><span class="w"> </span><span class="s">&quot;Tue&quot;</span><span class="p">,</span><span class="w"> </span><span class="s">&quot;Wed&quot;</span><span class="p">,</span><span class="w"> </span><span class="s">&quot;Thu&quot;</span><span class="p">,</span><span class="w"> </span><span class="s">&quot;Fri&quot;</span><span class="p">,</span><span class="w"> </span><span class="s">&quot;Sat&quot;</span><span class="p">,</span><span class="w"> </span><span class="s">&quot;Sun&quot;</span><span class="p" data-group-id="5619018071-5">]</span><span class="p">,</span><span class="w">
      </span><span class="nc">Enum</span><span class="o">.</span><span class="n">map</span><span class="p" data-group-id="5619018071-6">(</span><span class="p" data-group-id="5619018071-7">[</span><span class="w">
        </span><span class="p" data-group-id="5619018071-8">%{</span><span class="w">
          </span><span class="ss">name</span><span class="p">:</span><span class="w"> </span><span class="s">&quot;Email&quot;</span><span class="p">,</span><span class="w">
          </span><span class="ss">values</span><span class="p">:</span><span class="w"> </span><span class="p" data-group-id="5619018071-9">[</span><span class="mi">120</span><span class="p">,</span><span class="w"> </span><span class="mi">132</span><span class="p">,</span><span class="w"> </span><span class="mi">101</span><span class="p">,</span><span class="w"> </span><span class="mi">134</span><span class="p">,</span><span class="w"> </span><span class="mi">90</span><span class="p">,</span><span class="w"> </span><span class="mi">230</span><span class="p">,</span><span class="w"> </span><span class="mi">210</span><span class="p" data-group-id="5619018071-9">]</span><span class="w">
        </span><span class="p" data-group-id="5619018071-8">}</span><span class="p">,</span><span class="w">
        </span><span class="p" data-group-id="5619018071-10">%{</span><span class="w">
          </span><span class="ss">name</span><span class="p">:</span><span class="w"> </span><span class="s">&quot;Union Ads&quot;</span><span class="p">,</span><span class="w">
          </span><span class="ss">values</span><span class="p">:</span><span class="w"> </span><span class="p" data-group-id="5619018071-11">[</span><span class="mi">220</span><span class="p">,</span><span class="w"> </span><span class="mi">182</span><span class="p">,</span><span class="w"> </span><span class="mi">191</span><span class="p">,</span><span class="w"> </span><span class="mi">234</span><span class="p">,</span><span class="w"> </span><span class="mi">290</span><span class="p">,</span><span class="w"> </span><span class="mi">330</span><span class="p">,</span><span class="w"> </span><span class="mi">310</span><span class="p" data-group-id="5619018071-11">]</span><span class="w">
        </span><span class="p" data-group-id="5619018071-10">}</span><span class="p">,</span><span class="w">
        </span><span class="p" data-group-id="5619018071-12">%{</span><span class="w">
          </span><span class="ss">name</span><span class="p">:</span><span class="w"> </span><span class="s">&quot;Video Ads&quot;</span><span class="p">,</span><span class="w">
          </span><span class="ss">values</span><span class="p">:</span><span class="w"> </span><span class="p" data-group-id="5619018071-13">[</span><span class="mi">150</span><span class="p">,</span><span class="w"> </span><span class="mi">232</span><span class="p">,</span><span class="w"> </span><span class="mi">201</span><span class="p">,</span><span class="w"> </span><span class="mi">154</span><span class="p">,</span><span class="w"> </span><span class="mi">190</span><span class="p">,</span><span class="w"> </span><span class="mi">330</span><span class="p">,</span><span class="w"> </span><span class="mi">410</span><span class="p" data-group-id="5619018071-13">]</span><span class="w">
        </span><span class="p" data-group-id="5619018071-12">}</span><span class="p">,</span><span class="w">
        </span><span class="p" data-group-id="5619018071-14">%{</span><span class="w">
          </span><span class="ss">name</span><span class="p">:</span><span class="w"> </span><span class="s">&quot;Direct&quot;</span><span class="p">,</span><span class="w">
          </span><span class="ss">values</span><span class="p">:</span><span class="w"> </span><span class="p" data-group-id="5619018071-15">[</span><span class="mi">320</span><span class="p">,</span><span class="w"> </span><span class="mi">332</span><span class="p">,</span><span class="w"> </span><span class="mi">301</span><span class="p">,</span><span class="w"> </span><span class="mi">334</span><span class="p">,</span><span class="w"> </span><span class="mi">390</span><span class="p">,</span><span class="w"> </span><span class="mi">330</span><span class="p">,</span><span class="w"> </span><span class="mi">320</span><span class="p" data-group-id="5619018071-15">]</span><span class="p">,</span><span class="w">
        </span><span class="p" data-group-id="5619018071-14">}</span><span class="p">,</span><span class="w">
        </span><span class="p" data-group-id="5619018071-16">%{</span><span class="w">
          </span><span class="ss">name</span><span class="p">:</span><span class="w"> </span><span class="s">&quot;Search Engine&quot;</span><span class="p">,</span><span class="w">
          </span><span class="ss">values</span><span class="p">:</span><span class="w"> </span><span class="p" data-group-id="5619018071-17">[</span><span class="mi">820</span><span class="p">,</span><span class="w"> </span><span class="mi">932</span><span class="p">,</span><span class="w"> </span><span class="mi">901</span><span class="p">,</span><span class="w"> </span><span class="mi">934</span><span class="p">,</span><span class="w"> </span><span class="mi">1290</span><span class="p">,</span><span class="w"> </span><span class="mi">1330</span><span class="p">,</span><span class="w"> </span><span class="mi">1320</span><span class="p" data-group-id="5619018071-17">]</span><span class="w">
        </span><span class="p" data-group-id="5619018071-16">}</span><span class="w">
      </span><span class="p" data-group-id="5619018071-7">]</span><span class="p">,</span><span class="w"> </span><span class="k" data-group-id="5619018071-18">fn</span><span class="w"> </span><span class="n">line_data</span><span class="w"> </span><span class="o">-&gt;</span><span class="w">
        </span><span class="n">values</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">Enum</span><span class="o">.</span><span class="n">map</span><span class="p" data-group-id="5619018071-19">(</span><span class="n">line_data</span><span class="o">.</span><span class="n">values</span><span class="p">,</span><span class="w"> </span><span class="o">&amp;</span><span class="w"> </span><span class="nc">Float</span><span class="o">.</span><span class="n">round</span><span class="p" data-group-id="5619018071-20">(</span><span class="ni">&amp;1</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="nc">:rand</span><span class="o">.</span><span class="n">uniform</span><span class="p" data-group-id="5619018071-21">(</span><span class="p" data-group-id="5619018071-21">)</span><span class="p">,</span><span class="w"> </span><span class="mi">0</span><span class="p" data-group-id="5619018071-20">)</span><span class="p" data-group-id="5619018071-19">)</span><span class="w">
        </span><span class="n">values</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">cumulative</span><span class="w"> </span><span class="o">&amp;&amp;</span><span class="w"> </span><span class="nc">Enum</span><span class="o">.</span><span class="n">scan</span><span class="p" data-group-id="5619018071-22">(</span><span class="n">values</span><span class="p">,</span><span class="w"> </span><span class="mi">0</span><span class="p">,</span><span class="w"> </span><span class="o">&amp;</span><span class="w"> </span><span class="ni">&amp;2</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="ni">&amp;1</span><span class="p" data-group-id="5619018071-22">)</span><span class="w"> </span><span class="o">||</span><span class="w"> </span><span class="n">values</span><span class="w">

        </span><span class="p" data-group-id="5619018071-23">%{</span><span class="n">line_data</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="ss">values</span><span class="p">:</span><span class="w"> </span><span class="n">values</span><span class="p" data-group-id="5619018071-23">}</span><span class="w">
      </span><span class="k" data-group-id="5619018071-18">end</span><span class="p" data-group-id="5619018071-6">)</span><span class="w">
    </span><span class="p" data-group-id="5619018071-4">}</span><span class="w">
  </span><span class="k" data-group-id="5619018071-3">end</span><span class="w">

  </span><span class="kd">def</span><span class="w"> </span><span class="nf">get_gauge_multi_title_data</span><span class="w"> </span><span class="k" data-group-id="5619018071-24">do</span><span class="w">
    </span><span class="p" data-group-id="5619018071-25">%{</span><span class="w">
      </span><span class="ss">good</span><span class="p">:</span><span class="w"> </span><span class="nc">Float</span><span class="o">.</span><span class="n">round</span><span class="p" data-group-id="5619018071-26">(</span><span class="nc">:rand</span><span class="o">.</span><span class="n">uniform</span><span class="p" data-group-id="5619018071-27">(</span><span class="p" data-group-id="5619018071-27">)</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="mi">100</span><span class="p">,</span><span class="w"> </span><span class="mi">2</span><span class="p" data-group-id="5619018071-26">)</span><span class="p">,</span><span class="w">
      </span><span class="ss">better</span><span class="p">:</span><span class="w"> </span><span class="nc">Float</span><span class="o">.</span><span class="n">round</span><span class="p" data-group-id="5619018071-28">(</span><span class="nc">:rand</span><span class="o">.</span><span class="n">uniform</span><span class="p" data-group-id="5619018071-29">(</span><span class="p" data-group-id="5619018071-29">)</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="mi">100</span><span class="p">,</span><span class="w"> </span><span class="mi">2</span><span class="p" data-group-id="5619018071-28">)</span><span class="p">,</span><span class="w">
      </span><span class="ss">perfect</span><span class="p">:</span><span class="w"> </span><span class="nc">Float</span><span class="o">.</span><span class="n">round</span><span class="p" data-group-id="5619018071-30">(</span><span class="nc">:rand</span><span class="o">.</span><span class="n">uniform</span><span class="p" data-group-id="5619018071-31">(</span><span class="p" data-group-id="5619018071-31">)</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="mi">100</span><span class="p">,</span><span class="w"> </span><span class="mi">2</span><span class="p" data-group-id="5619018071-30">)</span><span class="w">
    </span><span class="p" data-group-id="5619018071-25">}</span><span class="w">
  </span><span class="k" data-group-id="5619018071-24">end</span><span class="w">

  </span><span class="kd">def</span><span class="w"> </span><span class="nf">get_process_count</span><span class="p">,</span><span class="w"> </span><span class="ss">do</span><span class="p">:</span><span class="w"> </span><span class="nc">:erlang</span><span class="o">.</span><span class="n">system_info</span><span class="p" data-group-id="5619018071-32">(</span><span class="ss">:process_count</span><span class="p" data-group-id="5619018071-32">)</span><span class="w">
</span><span class="k" data-group-id="5619018071-1">end</span></code></pre>
<p>
Remember to add a route to <code class="inline">DashboardLive</code> in the router:</p>
<pre><code class="makeup elixir"><span class="c1"># lib/my_app_web/router.ex</span><span class="w">
</span><span class="kd">defmodule</span><span class="w"> </span><span class="nc">MyAppWeb.Router</span><span class="w"> </span><span class="k" data-group-id="7458259015-1">do</span><span class="w">
  </span><span class="kn">use</span><span class="w"> </span><span class="nc">MyAppWeb</span><span class="p">,</span><span class="w"> </span><span class="ss">:router</span><span class="w">

  </span><span class="n">scope</span><span class="w"> </span><span class="s">&quot;/&quot;</span><span class="p">,</span><span class="w"> </span><span class="nc">MyAppWeb</span><span class="w"> </span><span class="k" data-group-id="7458259015-2">do</span><span class="w">
    </span><span class="n">pipe_through</span><span class="w"> </span><span class="ss">:browser</span><span class="w">

    </span><span class="n">live</span><span class="w"> </span><span class="s">&quot;/&quot;</span><span class="p">,</span><span class="w"> </span><span class="nc">DashboardLive</span><span class="w">
  </span><span class="k" data-group-id="7458259015-2">end</span><span class="w">
</span><span class="k" data-group-id="7458259015-1">end</span></code></pre>
<p>
Now onto the <code class="inline">DashboardLive</code> LiveView module:</p>
<pre><code class="makeup elixir"><span class="c1"># lib/my_app_web/live/dashboard_live.ex</span><span class="w">
</span><span class="kd">defmodule</span><span class="w"> </span><span class="nc">MyAppWeb.DashboardLive</span><span class="w"> </span><span class="k" data-group-id="4233177187-1">do</span><span class="w">
  </span><span class="kn">use</span><span class="w"> </span><span class="nc">MyAppWeb</span><span class="p">,</span><span class="w"> </span><span class="ss">:live_view</span><span class="w">

  </span><span class="kn">alias</span><span class="w"> </span><span class="nc">MyApp.Data</span><span class="w">
  </span><span class="kn">alias</span><span class="w"> </span><span class="nc">Phoenix.LiveView.AsyncResult</span><span class="w">

  </span><span class="na">@impl</span><span class="w"> </span><span class="no">true</span><span class="w">
  </span><span class="kd">def</span><span class="w"> </span><span class="nf">handle_params</span><span class="p" data-group-id="4233177187-2">(</span><span class="c">_params</span><span class="p">,</span><span class="w"> </span><span class="c">_uri</span><span class="p">,</span><span class="w"> </span><span class="n">socket</span><span class="p" data-group-id="4233177187-2">)</span><span class="w"> </span><span class="k" data-group-id="4233177187-3">do</span><span class="w">
    </span><span class="p" data-group-id="4233177187-4">{</span><span class="ss">:noreply</span><span class="p">,</span><span class="w">
      </span><span class="n">socket</span><span class="w">
      </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">assign</span><span class="p" data-group-id="4233177187-5">(</span><span class="ss">:options</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="4233177187-6">%{</span><span class="ss">line_stack</span><span class="p">:</span><span class="w"> </span><span class="p" data-group-id="4233177187-7">%{</span><span class="ss">cumulative</span><span class="p">:</span><span class="w"> </span><span class="no">true</span><span class="p" data-group-id="4233177187-7">}</span><span class="p" data-group-id="4233177187-6">}</span><span class="p" data-group-id="4233177187-5">)</span><span class="w">
      </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">assign</span><span class="p" data-group-id="4233177187-8">(</span><span class="ss">:reload_timers</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="4233177187-9">%{</span><span class="p" data-group-id="4233177187-9">}</span><span class="p" data-group-id="4233177187-8">)</span><span class="w">
      </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">assign_async_schedule</span><span class="p" data-group-id="4233177187-10">(</span><span class="p" data-group-id="4233177187-11">[</span><span class="ss">:line_stack</span><span class="p">,</span><span class="w"> </span><span class="ss">:gauge_multi_title</span><span class="p" data-group-id="4233177187-11">]</span><span class="p">,</span><span class="w"> </span><span class="k" data-group-id="4233177187-12">fn</span><span class="w"> </span><span class="n">opts</span><span class="w"> </span><span class="o">-&gt;</span><span class="w">
        </span><span class="p" data-group-id="4233177187-13">%{</span><span class="w">
          </span><span class="ss">line_stack</span><span class="p">:</span><span class="w"> </span><span class="nc">Data</span><span class="o">.</span><span class="n">get_line_stack_data</span><span class="p" data-group-id="4233177187-14">(</span><span class="n">opts</span><span class="p" data-group-id="4233177187-15">[</span><span class="ss">:line_stack</span><span class="p" data-group-id="4233177187-15">]</span><span class="p" data-group-id="4233177187-16">[</span><span class="ss">:cumulative</span><span class="p" data-group-id="4233177187-16">]</span><span class="p" data-group-id="4233177187-14">)</span><span class="p">,</span><span class="w">
          </span><span class="ss">gauge_multi_title</span><span class="p">:</span><span class="w"> </span><span class="nc">Data</span><span class="o">.</span><span class="n">get_gauge_multi_title_data</span><span class="p" data-group-id="4233177187-17">(</span><span class="p" data-group-id="4233177187-17">)</span><span class="w">
        </span><span class="p" data-group-id="4233177187-13">}</span><span class="w">
      </span><span class="k" data-group-id="4233177187-12">end</span><span class="p" data-group-id="4233177187-10">)</span><span class="w">
      </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">assign_async_schedule</span><span class="p" data-group-id="4233177187-18">(</span><span class="ss">:process_gauge</span><span class="p">,</span><span class="w"> </span><span class="k" data-group-id="4233177187-19">fn</span><span class="w"> </span><span class="c">_opts</span><span class="w"> </span><span class="o">-&gt;</span><span class="w">
        </span><span class="p" data-group-id="4233177187-20">%{</span><span class="ss">process_gauge</span><span class="p">:</span><span class="w"> </span><span class="nc">Data</span><span class="o">.</span><span class="n">get_process_count</span><span class="p" data-group-id="4233177187-21">(</span><span class="p" data-group-id="4233177187-21">)</span><span class="p" data-group-id="4233177187-20">}</span><span class="w">
      </span><span class="k" data-group-id="4233177187-19">end</span><span class="p" data-group-id="4233177187-18">)</span><span class="p" data-group-id="4233177187-4">}</span><span class="w">
  </span><span class="k" data-group-id="4233177187-3">end</span><span class="w">

  </span><span class="c1"># ...</span><span class="w">
</span><span class="k" data-group-id="4233177187-1">end</span></code></pre>
<p>
In the above, we are setting an <code class="inline">options</code> assign so we can switch between cumulative and single data points for our stacked line graph. We also assign the reload timers so we can cancel the scheduled poll when the options change. The <code class="inline">assign_async_schedule/4</code> function works the same way as <code class="inline">assign_async/3</code>, but also polls new data every 5 seconds.</p>
<p>
The reason we use polling instead of PubSub is that chart data is usually an aggregate of a lot of data, and it doesn’t make sense to listen to individual records. We only need to re-render the charts every once in a while.</p>
<p>
Continue to write out the <code class="inline">assign_async_schedule/4</code> logic:</p>
<pre><code class="makeup elixir"><span class="c1"># lib/my_app_web/live/dashboard_live.ex</span><span class="w">
</span><span class="kd">defmodule</span><span class="w"> </span><span class="nc">MyAppWeb.DashboardLive</span><span class="w"> </span><span class="k" data-group-id="2319748961-1">do</span><span class="w">
  </span><span class="c1"># ...</span><span class="w">

  </span><span class="kd">defp</span><span class="w"> </span><span class="nf">assign_async_schedule</span><span class="p" data-group-id="2319748961-2">(</span><span class="n">socket</span><span class="p">,</span><span class="w"> </span><span class="n">key_or_keys</span><span class="p">,</span><span class="w"> </span><span class="n">fun</span><span class="p">,</span><span class="w"> </span><span class="n">update_in</span><span class="w"> </span><span class="o">\\</span><span class="w"> </span><span class="nc">:timer</span><span class="o">.</span><span class="n">seconds</span><span class="p" data-group-id="2319748961-3">(</span><span class="mi">5</span><span class="p" data-group-id="2319748961-3">)</span><span class="p" data-group-id="2319748961-2">)</span><span class="w"> </span><span class="k" data-group-id="2319748961-4">do</span><span class="w">
    </span><span class="n">keys</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">List</span><span class="o">.</span><span class="n">wrap</span><span class="p" data-group-id="2319748961-5">(</span><span class="n">key_or_keys</span><span class="p" data-group-id="2319748961-5">)</span><span class="w">

    </span><span class="n">options</span><span class="w"> </span><span class="o">=</span><span class="w">
      </span><span class="n">keys</span><span class="w">
      </span><span class="o">|&gt;</span><span class="w"> </span><span class="nc">Enum</span><span class="o">.</span><span class="n">map</span><span class="p" data-group-id="2319748961-6">(</span><span class="o">&amp;</span><span class="w"> </span><span class="p" data-group-id="2319748961-7">{</span><span class="ni">&amp;1</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="2319748961-8">%{</span><span class="p" data-group-id="2319748961-8">}</span><span class="p" data-group-id="2319748961-7">}</span><span class="p" data-group-id="2319748961-6">)</span><span class="w">
      </span><span class="o">|&gt;</span><span class="w"> </span><span class="nc">Enum</span><span class="o">.</span><span class="n">into</span><span class="p" data-group-id="2319748961-9">(</span><span class="p" data-group-id="2319748961-10">%{</span><span class="p" data-group-id="2319748961-10">}</span><span class="p" data-group-id="2319748961-9">)</span><span class="w">
      </span><span class="o">|&gt;</span><span class="w"> </span><span class="nc">Map</span><span class="o">.</span><span class="n">merge</span><span class="p" data-group-id="2319748961-11">(</span><span class="n">socket</span><span class="o">.</span><span class="n">assigns</span><span class="o">.</span><span class="n">options</span><span class="p" data-group-id="2319748961-11">)</span><span class="w">

    </span><span class="n">socket</span><span class="w"> </span><span class="o">=</span><span class="w">
      </span><span class="n">socket</span><span class="w">
      </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">start_async_schedule</span><span class="p" data-group-id="2319748961-12">(</span><span class="n">key_or_keys</span><span class="p">,</span><span class="w"> </span><span class="n">fun</span><span class="p">,</span><span class="w"> </span><span class="n">update_in</span><span class="p" data-group-id="2319748961-12">)</span><span class="w">
      </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">assign</span><span class="p" data-group-id="2319748961-13">(</span><span class="ss">:options</span><span class="p">,</span><span class="w"> </span><span class="n">options</span><span class="p" data-group-id="2319748961-13">)</span><span class="w">

    </span><span class="nc">Enum</span><span class="o">.</span><span class="n">reduce</span><span class="p" data-group-id="2319748961-14">(</span><span class="n">keys</span><span class="p">,</span><span class="w"> </span><span class="n">socket</span><span class="p">,</span><span class="w"> </span><span class="k" data-group-id="2319748961-15">fn</span><span class="w"> </span><span class="n">key</span><span class="p">,</span><span class="w"> </span><span class="n">socket</span><span class="w"> </span><span class="o">-&gt;</span><span class="w">
      </span><span class="n">assign</span><span class="p" data-group-id="2319748961-16">(</span><span class="n">socket</span><span class="p">,</span><span class="w"> </span><span class="n">key</span><span class="p">,</span><span class="w"> </span><span class="nc">AsyncResult</span><span class="o">.</span><span class="n">loading</span><span class="p" data-group-id="2319748961-17">(</span><span class="p" data-group-id="2319748961-17">)</span><span class="p" data-group-id="2319748961-16">)</span><span class="w">
    </span><span class="k" data-group-id="2319748961-15">end</span><span class="p" data-group-id="2319748961-14">)</span><span class="w">
  </span><span class="k" data-group-id="2319748961-4">end</span><span class="w">

  </span><span class="kd">defp</span><span class="w"> </span><span class="nf">start_async_schedule</span><span class="p" data-group-id="2319748961-18">(</span><span class="n">socket</span><span class="p">,</span><span class="w"> </span><span class="n">key_or_keys</span><span class="p">,</span><span class="w"> </span><span class="n">fun</span><span class="p">,</span><span class="w"> </span><span class="n">update_in</span><span class="p" data-group-id="2319748961-18">)</span><span class="w"> </span><span class="k" data-group-id="2319748961-19">do</span><span class="w">
    </span><span class="n">options</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">Map</span><span class="o">.</span><span class="n">take</span><span class="p" data-group-id="2319748961-20">(</span><span class="n">socket</span><span class="o">.</span><span class="n">assigns</span><span class="o">.</span><span class="n">options</span><span class="p">,</span><span class="w"> </span><span class="nc">List</span><span class="o">.</span><span class="n">wrap</span><span class="p" data-group-id="2319748961-21">(</span><span class="n">key_or_keys</span><span class="p" data-group-id="2319748961-21">)</span><span class="p" data-group-id="2319748961-20">)</span><span class="w">

    </span><span class="n">start_async</span><span class="p" data-group-id="2319748961-22">(</span><span class="n">socket</span><span class="p">,</span><span class="w"> </span><span class="n">key_or_keys</span><span class="p">,</span><span class="w"> </span><span class="k" data-group-id="2319748961-23">fn</span><span class="w"> </span><span class="o">-&gt;</span><span class="w">
      </span><span class="p" data-group-id="2319748961-24">{</span><span class="n">fun</span><span class="o">.</span><span class="p" data-group-id="2319748961-25">(</span><span class="n">options</span><span class="p" data-group-id="2319748961-25">)</span><span class="p">,</span><span class="w"> </span><span class="n">fun</span><span class="p">,</span><span class="w"> </span><span class="n">update_in</span><span class="p" data-group-id="2319748961-24">}</span><span class="w">
    </span><span class="k" data-group-id="2319748961-23">end</span><span class="p" data-group-id="2319748961-22">)</span><span class="w">
  </span><span class="k" data-group-id="2319748961-19">end</span><span class="w">
  
  </span><span class="na">@impl</span><span class="w"> </span><span class="no">true</span><span class="w">
  </span><span class="kd">def</span><span class="w"> </span><span class="nf">handle_async</span><span class="p" data-group-id="2319748961-26">(</span><span class="n">key_or_keys</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="2319748961-27">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="2319748961-28">{</span><span class="n">values</span><span class="p">,</span><span class="w"> </span><span class="n">fun</span><span class="p">,</span><span class="w"> </span><span class="n">update_in</span><span class="p" data-group-id="2319748961-28">}</span><span class="p" data-group-id="2319748961-27">}</span><span class="p">,</span><span class="w"> </span><span class="n">socket</span><span class="p" data-group-id="2319748961-26">)</span><span class="w"> </span><span class="k" data-group-id="2319748961-29">do</span><span class="w">
    </span><span class="n">socket</span><span class="w"> </span><span class="o">=</span><span class="w">
      </span><span class="n">key_or_keys</span><span class="w">
      </span><span class="o">|&gt;</span><span class="w"> </span><span class="nc">List</span><span class="o">.</span><span class="n">wrap</span><span class="p" data-group-id="2319748961-30">(</span><span class="p" data-group-id="2319748961-30">)</span><span class="w">
      </span><span class="o">|&gt;</span><span class="w"> </span><span class="nc">Enum</span><span class="o">.</span><span class="n">reduce</span><span class="p" data-group-id="2319748961-31">(</span><span class="n">socket</span><span class="p">,</span><span class="w"> </span><span class="k" data-group-id="2319748961-32">fn</span><span class="w"> </span><span class="n">key</span><span class="p">,</span><span class="w"> </span><span class="n">socket</span><span class="w"> </span><span class="o">-&gt;</span><span class="w">
        </span><span class="n">async</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">Map</span><span class="o">.</span><span class="n">fetch!</span><span class="p" data-group-id="2319748961-33">(</span><span class="n">socket</span><span class="o">.</span><span class="n">assigns</span><span class="p">,</span><span class="w"> </span><span class="n">key</span><span class="p" data-group-id="2319748961-33">)</span><span class="w">
        </span><span class="n">value</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">Map</span><span class="o">.</span><span class="n">fetch!</span><span class="p" data-group-id="2319748961-34">(</span><span class="n">values</span><span class="p">,</span><span class="w"> </span><span class="n">key</span><span class="p" data-group-id="2319748961-34">)</span><span class="w">

        </span><span class="n">socket</span><span class="w">
        </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">push_chart_event</span><span class="p" data-group-id="2319748961-35">(</span><span class="n">key</span><span class="p">,</span><span class="w"> </span><span class="n">value</span><span class="p" data-group-id="2319748961-35">)</span><span class="w">
        </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">assign</span><span class="p" data-group-id="2319748961-36">(</span><span class="n">key</span><span class="p">,</span><span class="w"> </span><span class="nc">AsyncResult</span><span class="o">.</span><span class="n">ok</span><span class="p" data-group-id="2319748961-37">(</span><span class="n">async</span><span class="p">,</span><span class="w"> </span><span class="n">value</span><span class="p" data-group-id="2319748961-37">)</span><span class="p" data-group-id="2319748961-36">)</span><span class="w">
      </span><span class="k" data-group-id="2319748961-32">end</span><span class="p" data-group-id="2319748961-31">)</span><span class="w">
      </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">assign</span><span class="p" data-group-id="2319748961-38">(</span><span class="ss">:reload_timers</span><span class="p">,</span><span class="w"> </span><span class="nc">Map</span><span class="o">.</span><span class="n">put</span><span class="p" data-group-id="2319748961-39">(</span><span class="n">socket</span><span class="o">.</span><span class="n">assigns</span><span class="o">.</span><span class="n">reload_timers</span><span class="p">,</span><span class="w"> </span><span class="n">key_or_keys</span><span class="p">,</span><span class="w"> </span><span class="n">start_timer</span><span class="p" data-group-id="2319748961-40">(</span><span class="n">key_or_keys</span><span class="p">,</span><span class="w"> </span><span class="n">fun</span><span class="p">,</span><span class="w"> </span><span class="n">update_in</span><span class="p" data-group-id="2319748961-40">)</span><span class="p" data-group-id="2319748961-39">)</span><span class="p" data-group-id="2319748961-38">)</span><span class="w">

    </span><span class="p" data-group-id="2319748961-41">{</span><span class="ss">:noreply</span><span class="p">,</span><span class="w"> </span><span class="n">socket</span><span class="p" data-group-id="2319748961-41">}</span><span class="w">
  </span><span class="k" data-group-id="2319748961-29">end</span><span class="w">

  </span><span class="kd">def</span><span class="w"> </span><span class="nf">handle_async</span><span class="p" data-group-id="2319748961-42">(</span><span class="n">key_or_keys</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="2319748961-43">{</span><span class="ss">:exit</span><span class="p">,</span><span class="w"> </span><span class="n">reason</span><span class="p" data-group-id="2319748961-43">}</span><span class="w"> </span><span class="p">,</span><span class="w"> </span><span class="n">socket</span><span class="p" data-group-id="2319748961-42">)</span><span class="w"> </span><span class="k" data-group-id="2319748961-44">do</span><span class="w">
    </span><span class="n">socket</span><span class="w"> </span><span class="o">=</span><span class="w">
      </span><span class="n">key_or_keys</span><span class="w">
      </span><span class="o">|&gt;</span><span class="w"> </span><span class="nc">List</span><span class="o">.</span><span class="n">wrap</span><span class="p" data-group-id="2319748961-45">(</span><span class="p" data-group-id="2319748961-45">)</span><span class="w">
      </span><span class="o">|&gt;</span><span class="w"> </span><span class="nc">Enum</span><span class="o">.</span><span class="n">reduce</span><span class="p" data-group-id="2319748961-46">(</span><span class="n">socket</span><span class="p">,</span><span class="w"> </span><span class="k" data-group-id="2319748961-47">fn</span><span class="w"> </span><span class="n">key</span><span class="p">,</span><span class="w"> </span><span class="n">socket</span><span class="w"> </span><span class="o">-&gt;</span><span class="w">
        </span><span class="n">async</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">Map</span><span class="o">.</span><span class="n">fetch!</span><span class="p" data-group-id="2319748961-48">(</span><span class="n">socket</span><span class="o">.</span><span class="n">assigns</span><span class="p">,</span><span class="w"> </span><span class="n">key</span><span class="p" data-group-id="2319748961-48">)</span><span class="w">

        </span><span class="n">assign</span><span class="p" data-group-id="2319748961-49">(</span><span class="n">socket</span><span class="p">,</span><span class="w"> </span><span class="n">key</span><span class="p">,</span><span class="w"> </span><span class="nc">AsyncResult</span><span class="o">.</span><span class="n">failed</span><span class="p" data-group-id="2319748961-50">(</span><span class="n">async</span><span class="p">,</span><span class="w"> </span><span class="n">reason</span><span class="p" data-group-id="2319748961-50">)</span><span class="p" data-group-id="2319748961-49">)</span><span class="w">
      </span><span class="k" data-group-id="2319748961-47">end</span><span class="p" data-group-id="2319748961-46">)</span><span class="w">

    </span><span class="p" data-group-id="2319748961-51">{</span><span class="ss">:noreply</span><span class="p">,</span><span class="w"> </span><span class="n">socket</span><span class="p" data-group-id="2319748961-51">}</span><span class="w">
  </span><span class="k" data-group-id="2319748961-44">end</span><span class="w">

  </span><span class="kd">defp</span><span class="w"> </span><span class="nf">start_timer</span><span class="p" data-group-id="2319748961-52">(</span><span class="n">key_or_keys</span><span class="p">,</span><span class="w"> </span><span class="n">fun</span><span class="p">,</span><span class="w"> </span><span class="n">update_in</span><span class="p">,</span><span class="w"> </span><span class="n">send_after</span><span class="w"> </span><span class="o">\\</span><span class="w"> </span><span class="no">nil</span><span class="p" data-group-id="2319748961-52">)</span><span class="w"> </span><span class="k" data-group-id="2319748961-53">do</span><span class="w">
    </span><span class="n">ref</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">Process</span><span class="o">.</span><span class="n">send_after</span><span class="p" data-group-id="2319748961-54">(</span><span class="n">self</span><span class="p" data-group-id="2319748961-55">(</span><span class="p" data-group-id="2319748961-55">)</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="2319748961-56">{</span><span class="ss">:async</span><span class="p">,</span><span class="w"> </span><span class="n">key_or_keys</span><span class="p">,</span><span class="w"> </span><span class="n">fun</span><span class="p">,</span><span class="w"> </span><span class="n">update_in</span><span class="p" data-group-id="2319748961-56">}</span><span class="p">,</span><span class="w"> </span><span class="n">send_after</span><span class="w"> </span><span class="o">||</span><span class="w"> </span><span class="n">update_in</span><span class="p" data-group-id="2319748961-54">)</span><span class="w">

    </span><span class="p" data-group-id="2319748961-57">{</span><span class="n">ref</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="2319748961-58">{</span><span class="n">fun</span><span class="p">,</span><span class="w"> </span><span class="n">update_in</span><span class="p" data-group-id="2319748961-58">}</span><span class="p" data-group-id="2319748961-57">}</span><span class="w">
  </span><span class="k" data-group-id="2319748961-53">end</span><span class="w">

  </span><span class="na">@impl</span><span class="w"> </span><span class="no">true</span><span class="w">
  </span><span class="kd">def</span><span class="w"> </span><span class="nf">handle_info</span><span class="p" data-group-id="2319748961-59">(</span><span class="p" data-group-id="2319748961-60">{</span><span class="ss">:async</span><span class="p">,</span><span class="w"> </span><span class="n">key_or_keys</span><span class="p">,</span><span class="w"> </span><span class="n">fun</span><span class="p">,</span><span class="w"> </span><span class="n">update_in</span><span class="p" data-group-id="2319748961-60">}</span><span class="p">,</span><span class="w"> </span><span class="n">socket</span><span class="p" data-group-id="2319748961-59">)</span><span class="w"> </span><span class="k" data-group-id="2319748961-61">do</span><span class="w">
    </span><span class="n">socket</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">start_async_schedule</span><span class="p" data-group-id="2319748961-62">(</span><span class="n">socket</span><span class="p">,</span><span class="w"> </span><span class="n">key_or_keys</span><span class="p">,</span><span class="w"> </span><span class="n">fun</span><span class="p">,</span><span class="w"> </span><span class="n">update_in</span><span class="p" data-group-id="2319748961-62">)</span><span class="w">

    </span><span class="p" data-group-id="2319748961-63">{</span><span class="ss">:noreply</span><span class="p">,</span><span class="w"> </span><span class="n">socket</span><span class="p" data-group-id="2319748961-63">}</span><span class="w">
  </span><span class="k" data-group-id="2319748961-61">end</span><span class="w">

  </span><span class="c1"># ...</span><span class="w">
</span><span class="k" data-group-id="2319748961-1">end</span></code></pre>
<p>
We also need to handle the options by resetting the timer and loading new data immediately:</p>
<pre><code class="makeup elixir"><span class="c1"># lib/my_app_web/live/dashboard_live.ex</span><span class="w">
</span><span class="kd">defmodule</span><span class="w"> </span><span class="nc">MyAppWeb.DashboardLive</span><span class="w"> </span><span class="k" data-group-id="3678011120-1">do</span><span class="w">
  </span><span class="c1"># ...</span><span class="w">

  </span><span class="na">@impl</span><span class="w"> </span><span class="no">true</span><span class="w">
  </span><span class="kd">def</span><span class="w"> </span><span class="nf">handle_event</span><span class="p" data-group-id="3678011120-2">(</span><span class="s">&quot;options&quot;</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="3678011120-3">%{</span><span class="s">&quot;name&quot;</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="s">&quot;line_stack&quot;</span><span class="p">,</span><span class="w"> </span><span class="s">&quot;cumulative&quot;</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="n">cumulative</span><span class="p" data-group-id="3678011120-3">}</span><span class="p">,</span><span class="w"> </span><span class="n">socket</span><span class="p" data-group-id="3678011120-2">)</span><span class="w"> </span><span class="k" data-group-id="3678011120-4">do</span><span class="w">
    </span><span class="n">key_or_keys</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p" data-group-id="3678011120-5">[</span><span class="ss">:line_stack</span><span class="p">,</span><span class="w"> </span><span class="ss">:gauge_multi_title</span><span class="p" data-group-id="3678011120-5">]</span><span class="w">
    </span><span class="n">key_options</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">Map</span><span class="o">.</span><span class="n">put</span><span class="p" data-group-id="3678011120-6">(</span><span class="n">socket</span><span class="o">.</span><span class="n">assigns</span><span class="o">.</span><span class="n">options</span><span class="p" data-group-id="3678011120-7">[</span><span class="ss">:line_stack</span><span class="p" data-group-id="3678011120-7">]</span><span class="p">,</span><span class="w"> </span><span class="ss">:cumulative</span><span class="p">,</span><span class="w"> </span><span class="n">cumulative</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="s">&quot;true&quot;</span><span class="p" data-group-id="3678011120-6">)</span><span class="w">
    </span><span class="p" data-group-id="3678011120-8">{</span><span class="n">timer</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="3678011120-9">{</span><span class="n">fun</span><span class="p">,</span><span class="w"> </span><span class="n">update_in</span><span class="p" data-group-id="3678011120-9">}</span><span class="p" data-group-id="3678011120-8">}</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">Map</span><span class="o">.</span><span class="n">fetch!</span><span class="p" data-group-id="3678011120-10">(</span><span class="n">socket</span><span class="o">.</span><span class="n">assigns</span><span class="o">.</span><span class="n">reload_timers</span><span class="p">,</span><span class="w"> </span><span class="n">key_or_keys</span><span class="p" data-group-id="3678011120-10">)</span><span class="w">

    </span><span class="nc">Process</span><span class="o">.</span><span class="n">cancel_timer</span><span class="p" data-group-id="3678011120-11">(</span><span class="n">timer</span><span class="p" data-group-id="3678011120-11">)</span><span class="w">

    </span><span class="n">socket</span><span class="w"> </span><span class="o">=</span><span class="w">
      </span><span class="n">socket</span><span class="w">
      </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">assign</span><span class="p" data-group-id="3678011120-12">(</span><span class="ss">:options</span><span class="p">,</span><span class="w"> </span><span class="nc">Map</span><span class="o">.</span><span class="n">put</span><span class="p" data-group-id="3678011120-13">(</span><span class="n">socket</span><span class="o">.</span><span class="n">assigns</span><span class="o">.</span><span class="n">options</span><span class="p">,</span><span class="w"> </span><span class="ss">:line_stack</span><span class="p">,</span><span class="w"> </span><span class="n">key_options</span><span class="p" data-group-id="3678011120-13">)</span><span class="p" data-group-id="3678011120-12">)</span><span class="w">
      </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">assign</span><span class="p" data-group-id="3678011120-14">(</span><span class="ss">:reload_timers</span><span class="p">,</span><span class="w"> </span><span class="nc">Map</span><span class="o">.</span><span class="n">put</span><span class="p" data-group-id="3678011120-15">(</span><span class="n">socket</span><span class="o">.</span><span class="n">assigns</span><span class="o">.</span><span class="n">reload_timers</span><span class="p">,</span><span class="w"> </span><span class="n">key_or_keys</span><span class="p">,</span><span class="w"> </span><span class="n">start_timer</span><span class="p" data-group-id="3678011120-16">(</span><span class="n">key_or_keys</span><span class="p">,</span><span class="w"> </span><span class="n">fun</span><span class="p">,</span><span class="w"> </span><span class="n">update_in</span><span class="p">,</span><span class="w"> </span><span class="mi">1</span><span class="p" data-group-id="3678011120-16">)</span><span class="p" data-group-id="3678011120-15">)</span><span class="p" data-group-id="3678011120-14">)</span><span class="w">

    </span><span class="p" data-group-id="3678011120-17">{</span><span class="ss">:noreply</span><span class="p">,</span><span class="w"> </span><span class="n">socket</span><span class="p" data-group-id="3678011120-17">}</span><span class="w">
  </span><span class="k" data-group-id="3678011120-4">end</span><span class="w">

  </span><span class="c1"># ...</span><span class="w">
</span><span class="k" data-group-id="3678011120-1">end</span></code></pre>
<p>
All that’s left to do is to push the ECharts options:</p>
<pre><code class="makeup elixir"><span class="c1"># lib/my_app_web/live/dashboard_live.ex</span><span class="w">
</span><span class="kd">defmodule</span><span class="w"> </span><span class="nc">MyAppWeb.DashboardLive</span><span class="w"> </span><span class="k" data-group-id="1523962655-1">do</span><span class="w">
  </span><span class="c1"># ...</span><span class="w">

  </span><span class="kd">defp</span><span class="w"> </span><span class="nf">push_chart_event</span><span class="p" data-group-id="1523962655-2">(</span><span class="n">socket</span><span class="p">,</span><span class="w"> </span><span class="ss">:line_stack</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="1523962655-3">{</span><span class="n">columns</span><span class="p">,</span><span class="w"> </span><span class="n">data</span><span class="p" data-group-id="1523962655-3">}</span><span class="p" data-group-id="1523962655-2">)</span><span class="w"> </span><span class="k" data-group-id="1523962655-4">do</span><span class="w">
    </span><span class="n">option</span><span class="w"> </span><span class="o">=</span><span class="w">
      </span><span class="p" data-group-id="1523962655-5">%{</span><span class="w">
        </span><span class="ss">tooltip</span><span class="p">:</span><span class="w"> </span><span class="p" data-group-id="1523962655-6">%{</span><span class="w">
          </span><span class="ss">trigger</span><span class="p">:</span><span class="w"> </span><span class="s">&quot;axis&quot;</span><span class="w">
        </span><span class="p" data-group-id="1523962655-6">}</span><span class="p">,</span><span class="w">
        </span><span class="ss">legend</span><span class="p">:</span><span class="w"> </span><span class="p" data-group-id="1523962655-7">%{</span><span class="w">
          </span><span class="ss">data</span><span class="p">:</span><span class="w"> </span><span class="nc">Enum</span><span class="o">.</span><span class="n">map</span><span class="p" data-group-id="1523962655-8">(</span><span class="n">data</span><span class="p">,</span><span class="w"> </span><span class="o">&amp;</span><span class="w"> </span><span class="ni">&amp;1</span><span class="o">.</span><span class="n">name</span><span class="p" data-group-id="1523962655-8">)</span><span class="p">,</span><span class="w">
          </span><span class="ss">selected</span><span class="p">:</span><span class="w"> </span><span class="p" data-group-id="1523962655-9">%{</span><span class="s">&quot;Email&quot;</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="no">false</span><span class="p" data-group-id="1523962655-9">}</span><span class="w">
        </span><span class="p" data-group-id="1523962655-7">}</span><span class="p">,</span><span class="w">
        </span><span class="ss">grid</span><span class="p">:</span><span class="w"> </span><span class="p" data-group-id="1523962655-10">%{</span><span class="w">
          </span><span class="ss">left</span><span class="p">:</span><span class="w"> </span><span class="s">&quot;3%&quot;</span><span class="p">,</span><span class="w">
          </span><span class="ss">right</span><span class="p">:</span><span class="w"> </span><span class="s">&quot;4%&quot;</span><span class="p">,</span><span class="w">
          </span><span class="ss">bottom</span><span class="p">:</span><span class="w"> </span><span class="s">&quot;3%&quot;</span><span class="p">,</span><span class="w">
          </span><span class="ss">containLabel</span><span class="p">:</span><span class="w"> </span><span class="no">true</span><span class="w">
        </span><span class="p" data-group-id="1523962655-10">}</span><span class="p">,</span><span class="w">
        </span><span class="ss">toolbox</span><span class="p">:</span><span class="w"> </span><span class="p" data-group-id="1523962655-11">%{</span><span class="w">
          </span><span class="ss">feature</span><span class="p">:</span><span class="w"> </span><span class="p" data-group-id="1523962655-12">%{</span><span class="w">
            </span><span class="ss">saveAsImage</span><span class="p">:</span><span class="w"> </span><span class="p" data-group-id="1523962655-13">%{</span><span class="p" data-group-id="1523962655-13">}</span><span class="w">
          </span><span class="p" data-group-id="1523962655-12">}</span><span class="w">
        </span><span class="p" data-group-id="1523962655-11">}</span><span class="p">,</span><span class="w">
        </span><span class="ss">xAxis</span><span class="p">:</span><span class="w"> </span><span class="p" data-group-id="1523962655-14">%{</span><span class="w">
          </span><span class="ss">type</span><span class="p">:</span><span class="w"> </span><span class="s">&quot;category&quot;</span><span class="p">,</span><span class="w">
          </span><span class="ss">boundaryGap</span><span class="p">:</span><span class="w"> </span><span class="no">false</span><span class="p">,</span><span class="w">
          </span><span class="ss">data</span><span class="p">:</span><span class="w"> </span><span class="n">columns</span><span class="w">
        </span><span class="p" data-group-id="1523962655-14">}</span><span class="p">,</span><span class="w">
        </span><span class="ss">yAxis</span><span class="p">:</span><span class="w"> </span><span class="p" data-group-id="1523962655-15">%{</span><span class="w">
          </span><span class="ss">type</span><span class="p">:</span><span class="w"> </span><span class="s">&quot;value&quot;</span><span class="w">
        </span><span class="p" data-group-id="1523962655-15">}</span><span class="p">,</span><span class="w">
        </span><span class="ss">series</span><span class="p">:</span><span class="w"> </span><span class="nc">Enum</span><span class="o">.</span><span class="n">map</span><span class="p" data-group-id="1523962655-16">(</span><span class="n">data</span><span class="p">,</span><span class="w"> </span><span class="o">&amp;</span><span class="p" data-group-id="1523962655-17">%{</span><span class="w">
            </span><span class="ss">name</span><span class="p">:</span><span class="w"> </span><span class="ni">&amp;1</span><span class="o">.</span><span class="n">name</span><span class="p">,</span><span class="w">
            </span><span class="ss">type</span><span class="p">:</span><span class="w"> </span><span class="s">&quot;line&quot;</span><span class="p">,</span><span class="w">
            </span><span class="ss">stack</span><span class="p">:</span><span class="w"> </span><span class="s">&quot;Total&quot;</span><span class="p">,</span><span class="w">
            </span><span class="ss">data</span><span class="p">:</span><span class="w"> </span><span class="ni">&amp;1</span><span class="o">.</span><span class="n">values</span><span class="w">
          </span><span class="p" data-group-id="1523962655-17">}</span><span class="p" data-group-id="1523962655-16">)</span><span class="w">
      </span><span class="p" data-group-id="1523962655-5">}</span><span class="w">

    </span><span class="n">push_event</span><span class="p" data-group-id="1523962655-18">(</span><span class="n">socket</span><span class="p">,</span><span class="w"> </span><span class="s">&quot;chart-option-line_stack&quot;</span><span class="p">,</span><span class="w"> </span><span class="n">option</span><span class="p" data-group-id="1523962655-18">)</span><span class="w">
  </span><span class="k" data-group-id="1523962655-4">end</span><span class="w">

  </span><span class="kd">defp</span><span class="w"> </span><span class="nf">push_chart_event</span><span class="p" data-group-id="1523962655-19">(</span><span class="n">socket</span><span class="p">,</span><span class="w"> </span><span class="ss">:gauge_multi_title</span><span class="p">,</span><span class="w"> </span><span class="n">data</span><span class="p" data-group-id="1523962655-19">)</span><span class="w"> </span><span class="k" data-group-id="1523962655-20">do</span><span class="w">
    </span><span class="n">option</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p" data-group-id="1523962655-21">%{</span><span class="w">
      </span><span class="ss">series</span><span class="p">:</span><span class="w"> </span><span class="p" data-group-id="1523962655-22">[</span><span class="w">
        </span><span class="p" data-group-id="1523962655-23">%{</span><span class="w">
          </span><span class="ss">type</span><span class="p">:</span><span class="w"> </span><span class="s">&quot;gauge&quot;</span><span class="p">,</span><span class="w">
          </span><span class="ss">anchor</span><span class="p">:</span><span class="w"> </span><span class="p" data-group-id="1523962655-24">%{</span><span class="w">
            </span><span class="ss">show</span><span class="p">:</span><span class="w"> </span><span class="no">true</span><span class="p">,</span><span class="w">
            </span><span class="ss">showAbove</span><span class="p">:</span><span class="w"> </span><span class="no">true</span><span class="p">,</span><span class="w">
            </span><span class="ss">size</span><span class="p">:</span><span class="w"> </span><span class="mi">18</span><span class="p">,</span><span class="w">
            </span><span class="ss">itemStyle</span><span class="p">:</span><span class="w"> </span><span class="p" data-group-id="1523962655-25">%{</span><span class="w">
              </span><span class="ss">color</span><span class="p">:</span><span class="w"> </span><span class="s">&quot;#FAC858&quot;</span><span class="w">
            </span><span class="p" data-group-id="1523962655-25">}</span><span class="w">
          </span><span class="p" data-group-id="1523962655-24">}</span><span class="p">,</span><span class="w">
          </span><span class="ss">pointer</span><span class="p">:</span><span class="w"> </span><span class="p" data-group-id="1523962655-26">%{</span><span class="w">
            </span><span class="ss">icon</span><span class="p">:</span><span class="w"> </span><span class="s">&quot;path://M2.9,0.7L2.9,0.7c1.4,0,2.6,1.2,2.6,2.6v115c0,1.4-1.2,2.6-2.6,2.6l0,0c-1.4,0-2.6-1.2-2.6-2.6V3.3C0.3,1.9,1.4,0.7,2.9,0.7z&quot;</span><span class="p">,</span><span class="w">
            </span><span class="ss">width</span><span class="p">:</span><span class="w"> </span><span class="mi">8</span><span class="p">,</span><span class="w">
            </span><span class="ss">length</span><span class="p">:</span><span class="w"> </span><span class="s">&quot;80%&quot;</span><span class="p">,</span><span class="w">
            </span><span class="ss">offsetCenter</span><span class="p">:</span><span class="w"> </span><span class="p" data-group-id="1523962655-27">[</span><span class="mi">0</span><span class="p">,</span><span class="w"> </span><span class="s">&quot;8%&quot;</span><span class="p" data-group-id="1523962655-27">]</span><span class="w">
          </span><span class="p" data-group-id="1523962655-26">}</span><span class="p">,</span><span class="w">
          </span><span class="ss">progress</span><span class="p">:</span><span class="w"> </span><span class="p" data-group-id="1523962655-28">%{</span><span class="w">
            </span><span class="ss">show</span><span class="p">:</span><span class="w"> </span><span class="no">true</span><span class="p">,</span><span class="w">
            </span><span class="ss">overlap</span><span class="p">:</span><span class="w"> </span><span class="no">true</span><span class="p">,</span><span class="w">
            </span><span class="ss">roundCap</span><span class="p">:</span><span class="w"> </span><span class="no">true</span><span class="w">
          </span><span class="p" data-group-id="1523962655-28">}</span><span class="p">,</span><span class="w">
          </span><span class="ss">axisLine</span><span class="p">:</span><span class="w"> </span><span class="p" data-group-id="1523962655-29">%{</span><span class="w">
            </span><span class="ss">roundCap</span><span class="p">:</span><span class="w"> </span><span class="no">true</span><span class="w">
          </span><span class="p" data-group-id="1523962655-29">}</span><span class="p">,</span><span class="w">
          </span><span class="ss">data</span><span class="p">:</span><span class="w"> </span><span class="p" data-group-id="1523962655-30">[</span><span class="w">
            </span><span class="p" data-group-id="1523962655-31">%{</span><span class="w">
              </span><span class="ss">value</span><span class="p">:</span><span class="w"> </span><span class="n">data</span><span class="o">.</span><span class="n">good</span><span class="p">,</span><span class="w">
              </span><span class="ss">name</span><span class="p">:</span><span class="w"> </span><span class="s">&quot;Good&quot;</span><span class="p">,</span><span class="w">
              </span><span class="ss">title</span><span class="p">:</span><span class="w"> </span><span class="p" data-group-id="1523962655-32">%{</span><span class="w">
                </span><span class="ss">offsetCenter</span><span class="p">:</span><span class="w"> </span><span class="p" data-group-id="1523962655-33">[</span><span class="s">&quot;-40%&quot;</span><span class="p">,</span><span class="w"> </span><span class="s">&quot;80%&quot;</span><span class="p" data-group-id="1523962655-33">]</span><span class="p">,</span><span class="w">
                </span><span class="ss">fontSize</span><span class="p">:</span><span class="w"> </span><span class="mi">10</span><span class="w">
              </span><span class="p" data-group-id="1523962655-32">}</span><span class="p">,</span><span class="w">
              </span><span class="ss">detail</span><span class="p">:</span><span class="w"> </span><span class="p" data-group-id="1523962655-34">%{</span><span class="w">
                </span><span class="ss">offsetCenter</span><span class="p">:</span><span class="w"> </span><span class="p" data-group-id="1523962655-35">[</span><span class="s">&quot;-40%&quot;</span><span class="p">,</span><span class="w"> </span><span class="s">&quot;95%&quot;</span><span class="p" data-group-id="1523962655-35">]</span><span class="p">,</span><span class="w">
                </span><span class="ss">fontSize</span><span class="p">:</span><span class="w"> </span><span class="mi">10</span><span class="p">,</span><span class="w">
                </span><span class="ss">height</span><span class="p">:</span><span class="w"> </span><span class="mi">10</span><span class="p">,</span><span class="w">
                </span><span class="ss">width</span><span class="p">:</span><span class="w"> </span><span class="mi">20</span><span class="w">
              </span><span class="p" data-group-id="1523962655-34">}</span><span class="w">
            </span><span class="p" data-group-id="1523962655-31">}</span><span class="p">,</span><span class="w">
            </span><span class="p" data-group-id="1523962655-36">%{</span><span class="w">
              </span><span class="ss">value</span><span class="p">:</span><span class="w"> </span><span class="n">data</span><span class="o">.</span><span class="n">better</span><span class="p">,</span><span class="w">
              </span><span class="ss">name</span><span class="p">:</span><span class="w"> </span><span class="s">&quot;Better&quot;</span><span class="p">,</span><span class="w">
              </span><span class="ss">title</span><span class="p">:</span><span class="w"> </span><span class="p" data-group-id="1523962655-37">%{</span><span class="w">
                </span><span class="ss">offsetCenter</span><span class="p">:</span><span class="w"> </span><span class="p" data-group-id="1523962655-38">[</span><span class="s">&quot;0%&quot;</span><span class="p">,</span><span class="w"> </span><span class="s">&quot;80%&quot;</span><span class="p" data-group-id="1523962655-38">]</span><span class="p">,</span><span class="w">
                </span><span class="ss">fontSize</span><span class="p">:</span><span class="w"> </span><span class="mi">10</span><span class="w">
              </span><span class="p" data-group-id="1523962655-37">}</span><span class="p">,</span><span class="w">
              </span><span class="ss">detail</span><span class="p">:</span><span class="w"> </span><span class="p" data-group-id="1523962655-39">%{</span><span class="w">
                </span><span class="ss">offsetCenter</span><span class="p">:</span><span class="w"> </span><span class="p" data-group-id="1523962655-40">[</span><span class="s">&quot;0%&quot;</span><span class="p">,</span><span class="w"> </span><span class="s">&quot;95%&quot;</span><span class="p" data-group-id="1523962655-40">]</span><span class="p">,</span><span class="w">
                </span><span class="ss">fontSize</span><span class="p">:</span><span class="w"> </span><span class="mi">10</span><span class="p">,</span><span class="w">
                </span><span class="ss">height</span><span class="p">:</span><span class="w"> </span><span class="mi">10</span><span class="p">,</span><span class="w">
                </span><span class="ss">width</span><span class="p">:</span><span class="w"> </span><span class="mi">20</span><span class="w">
              </span><span class="p" data-group-id="1523962655-39">}</span><span class="w">
            </span><span class="p" data-group-id="1523962655-36">}</span><span class="p">,</span><span class="w">
            </span><span class="p" data-group-id="1523962655-41">%{</span><span class="w">
              </span><span class="ss">value</span><span class="p">:</span><span class="w"> </span><span class="n">data</span><span class="o">.</span><span class="n">perfect</span><span class="p">,</span><span class="w">
              </span><span class="ss">name</span><span class="p">:</span><span class="w"> </span><span class="s">&quot;Perfect&quot;</span><span class="p">,</span><span class="w">
              </span><span class="ss">title</span><span class="p">:</span><span class="w"> </span><span class="p" data-group-id="1523962655-42">%{</span><span class="w">
                </span><span class="ss">offsetCenter</span><span class="p">:</span><span class="w"> </span><span class="p" data-group-id="1523962655-43">[</span><span class="s">&quot;40%&quot;</span><span class="p">,</span><span class="w"> </span><span class="s">&quot;80%&quot;</span><span class="p" data-group-id="1523962655-43">]</span><span class="p">,</span><span class="w">
                </span><span class="ss">fontSize</span><span class="p">:</span><span class="w"> </span><span class="mi">10</span><span class="w">
              </span><span class="p" data-group-id="1523962655-42">}</span><span class="p">,</span><span class="w">
              </span><span class="ss">detail</span><span class="p">:</span><span class="w"> </span><span class="p" data-group-id="1523962655-44">%{</span><span class="w">
                </span><span class="ss">offsetCenter</span><span class="p">:</span><span class="w"> </span><span class="p" data-group-id="1523962655-45">[</span><span class="s">&quot;40%&quot;</span><span class="p">,</span><span class="w"> </span><span class="s">&quot;95%&quot;</span><span class="p" data-group-id="1523962655-45">]</span><span class="p">,</span><span class="w">
                </span><span class="ss">fontSize</span><span class="p">:</span><span class="w"> </span><span class="mi">10</span><span class="p">,</span><span class="w">
                </span><span class="ss">height</span><span class="p">:</span><span class="w"> </span><span class="mi">10</span><span class="p">,</span><span class="w">
                </span><span class="ss">width</span><span class="p">:</span><span class="w"> </span><span class="mi">20</span><span class="w">
              </span><span class="p" data-group-id="1523962655-44">}</span><span class="w">
            </span><span class="p" data-group-id="1523962655-41">}</span><span class="w">
          </span><span class="p" data-group-id="1523962655-30">]</span><span class="p">,</span><span class="w">
          </span><span class="ss">title</span><span class="p">:</span><span class="w"> </span><span class="p" data-group-id="1523962655-46">%{</span><span class="w">
            </span><span class="ss">fontSize</span><span class="p">:</span><span class="w"> </span><span class="mi">14</span><span class="w">
          </span><span class="p" data-group-id="1523962655-46">}</span><span class="p">,</span><span class="w">
          </span><span class="ss">detail</span><span class="p">:</span><span class="w"> </span><span class="p" data-group-id="1523962655-47">%{</span><span class="w">
            </span><span class="ss">width</span><span class="p">:</span><span class="w"> </span><span class="mi">40</span><span class="p">,</span><span class="w">
            </span><span class="ss">height</span><span class="p">:</span><span class="w"> </span><span class="mi">14</span><span class="p">,</span><span class="w">
            </span><span class="ss">fontSize</span><span class="p">:</span><span class="w"> </span><span class="mi">14</span><span class="p">,</span><span class="w">
            </span><span class="ss">color</span><span class="p">:</span><span class="w"> </span><span class="s">&quot;#fff&quot;</span><span class="p">,</span><span class="w">
            </span><span class="ss">backgroundColor</span><span class="p">:</span><span class="w"> </span><span class="s">&quot;inherit&quot;</span><span class="p">,</span><span class="w">
            </span><span class="ss">borderRadius</span><span class="p">:</span><span class="w"> </span><span class="mi">3</span><span class="p">,</span><span class="w">
            </span><span class="ss">formatter</span><span class="p">:</span><span class="w"> </span><span class="s">&quot;{value}%&quot;</span><span class="w">
          </span><span class="p" data-group-id="1523962655-47">}</span><span class="w">
        </span><span class="p" data-group-id="1523962655-23">}</span><span class="w">
      </span><span class="p" data-group-id="1523962655-22">]</span><span class="w">
    </span><span class="p" data-group-id="1523962655-21">}</span><span class="w">

    </span><span class="n">push_event</span><span class="p" data-group-id="1523962655-48">(</span><span class="n">socket</span><span class="p">,</span><span class="w"> </span><span class="s">&quot;chart-option-guage_multi_title&quot;</span><span class="p">,</span><span class="w"> </span><span class="n">option</span><span class="p" data-group-id="1523962655-48">)</span><span class="w">
  </span><span class="k" data-group-id="1523962655-20">end</span><span class="w">

  </span><span class="kd">defp</span><span class="w"> </span><span class="nf">push_chart_event</span><span class="p" data-group-id="1523962655-49">(</span><span class="n">socket</span><span class="p">,</span><span class="w"> </span><span class="ss">:process_gauge</span><span class="p">,</span><span class="w"> </span><span class="n">data</span><span class="p" data-group-id="1523962655-49">)</span><span class="w"> </span><span class="k" data-group-id="1523962655-50">do</span><span class="w">
    </span><span class="n">option</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p" data-group-id="1523962655-51">%{</span><span class="w">
      </span><span class="ss">series</span><span class="p">:</span><span class="w"> </span><span class="p" data-group-id="1523962655-52">[</span><span class="w">
        </span><span class="p" data-group-id="1523962655-53">%{</span><span class="w">
          </span><span class="ss">name</span><span class="p">:</span><span class="w"> </span><span class="s">&quot;Processes&quot;</span><span class="p">,</span><span class="w">
          </span><span class="ss">type</span><span class="p">:</span><span class="w"> </span><span class="s">&quot;gauge&quot;</span><span class="p">,</span><span class="w">
          </span><span class="ss">data</span><span class="p">:</span><span class="w"> </span><span class="p" data-group-id="1523962655-54">[</span><span class="w">
            </span><span class="p" data-group-id="1523962655-55">%{</span><span class="w">
              </span><span class="ss">value</span><span class="p">:</span><span class="w"> </span><span class="n">data</span><span class="w">
            </span><span class="p" data-group-id="1523962655-55">}</span><span class="w">
          </span><span class="p" data-group-id="1523962655-54">]</span><span class="w">
        </span><span class="p" data-group-id="1523962655-53">}</span><span class="w">
      </span><span class="p" data-group-id="1523962655-52">]</span><span class="w">
    </span><span class="p" data-group-id="1523962655-51">}</span><span class="w">

    </span><span class="n">push_event</span><span class="p" data-group-id="1523962655-56">(</span><span class="n">socket</span><span class="p">,</span><span class="w"> </span><span class="s">&quot;chart-option-process_gauge&quot;</span><span class="p">,</span><span class="w"> </span><span class="n">option</span><span class="p" data-group-id="1523962655-56">)</span><span class="w">
  </span><span class="k" data-group-id="1523962655-50">end</span><span class="w">
</span><span class="k" data-group-id="1523962655-1">end</span></code></pre>
<p>
We’re setting up all the chart options here and pushing them as an event. This allows us to dynamically configure everything from the backend. We can even switch the chart type ad-hoc.</p>
<p>
All that’s left to do is to set up the template:</p>
<pre><code class="makeup elixir"><span class="o">&lt;</span><span class="p">%</span><span class="o">!</span><span class="o">--</span><span class="w"> </span><span class="c1"># lib/my_app_web/live/dashboard_live.html.heex --%&gt;</span><span class="w">
</span><span class="o">&lt;</span><span class="n">div</span><span class="w"> </span><span class="n">class</span><span class="o">=</span><span class="s">&quot;w-full grid grid-row gap-4&quot;</span><span class="o">&gt;</span><span class="w">
  </span><span class="o">&lt;</span><span class="n">div</span><span class="w"> </span><span class="n">class</span><span class="o">=</span><span class="s">&quot;w-full grid grid-row gap-4 p-4&quot;</span><span class="o">&gt;</span><span class="w">
    </span><span class="o">&lt;</span><span class="o">.</span><span class="n">header</span><span class="o">&gt;</span><span class="w">
      </span><span class="nc">Line</span><span class="w"> </span><span class="n">stack</span><span class="w">
      </span><span class="o">&lt;</span><span class="ss">:subtitle</span><span class="o">&gt;</span><span class="w">
        </span><span class="o">&lt;</span><span class="o">.</span><span class="n">button</span><span class="w"> </span><span class="ss">:if</span><span class="o">=</span><span class="p" data-group-id="5401799441-1">{</span><span class="na">@options</span><span class="p" data-group-id="5401799441-2">[</span><span class="ss">:line_stack</span><span class="p" data-group-id="5401799441-2">]</span><span class="p" data-group-id="5401799441-3">[</span><span class="ss">:cumulative</span><span class="p" data-group-id="5401799441-3">]</span><span class="p" data-group-id="5401799441-1">}</span><span class="w"> </span><span class="n">phx</span><span class="o">-</span><span class="n">click</span><span class="o">=</span><span class="s">&quot;options&quot;</span><span class="w"> </span><span class="n">phx</span><span class="o">-</span><span class="n">value</span><span class="o">-</span><span class="n">name</span><span class="o">=</span><span class="p" data-group-id="5401799441-4">{</span><span class="ss">:line_stack</span><span class="p" data-group-id="5401799441-4">}</span><span class="w"> </span><span class="n">phx</span><span class="o">-</span><span class="n">value</span><span class="o">-</span><span class="n">cumulative</span><span class="o">=</span><span class="s">&quot;false&quot;</span><span class="o">&gt;</span><span class="w">
          </span><span class="nc">Single</span><span class="w">
        </span><span class="o">&lt;</span><span class="o">/</span><span class="o">.</span><span class="n">button</span><span class="o">&gt;</span><span class="w">
        </span><span class="o">&lt;</span><span class="o">.</span><span class="n">button</span><span class="w"> </span><span class="ss">:if</span><span class="o">=</span><span class="p" data-group-id="5401799441-5">{</span><span class="na">@options</span><span class="p" data-group-id="5401799441-6">[</span><span class="ss">:line_stack</span><span class="p" data-group-id="5401799441-6">]</span><span class="p" data-group-id="5401799441-7">[</span><span class="ss">:cumulative</span><span class="p" data-group-id="5401799441-7">]</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="no">true</span><span class="p" data-group-id="5401799441-5">}</span><span class="w"> </span><span class="n">phx</span><span class="o">-</span><span class="n">click</span><span class="o">=</span><span class="s">&quot;options&quot;</span><span class="w"> </span><span class="n">phx</span><span class="o">-</span><span class="n">value</span><span class="o">-</span><span class="n">name</span><span class="o">=</span><span class="p" data-group-id="5401799441-8">{</span><span class="ss">:line_stack</span><span class="p" data-group-id="5401799441-8">}</span><span class="w"> </span><span class="n">phx</span><span class="o">-</span><span class="n">value</span><span class="o">-</span><span class="n">cumulative</span><span class="o">=</span><span class="s">&quot;true&quot;</span><span class="o">&gt;</span><span class="w">
          </span><span class="nc">Rolling</span><span class="w">
        </span><span class="o">&lt;</span><span class="o">/</span><span class="o">.</span><span class="n">button</span><span class="o">&gt;</span><span class="w">
      </span><span class="o">&lt;</span><span class="o">/</span><span class="ss">:subtitle</span><span class="o">&gt;</span><span class="w">
    </span><span class="o">&lt;</span><span class="o">/</span><span class="o">.</span><span class="n">header</span><span class="o">&gt;</span><span class="w">

    </span><span class="o">&lt;</span><span class="o">.</span><span class="n">async_result</span><span class="w"> </span><span class="n">assign</span><span class="o">=</span><span class="p" data-group-id="5401799441-9">{</span><span class="na">@line_stack</span><span class="p" data-group-id="5401799441-9">}</span><span class="o">&gt;</span><span class="w">
      </span><span class="o">&lt;</span><span class="ss">:loading</span><span class="o">&gt;</span><span class="nc">Loading</span><span class="n">...</span><span class="o">&lt;</span><span class="o">/</span><span class="ss">:loading</span><span class="o">&gt;</span><span class="w">
      </span><span class="o">&lt;</span><span class="ss">:failed</span><span class="o">&gt;</span><span class="o">&lt;</span><span class="o">.</span><span class="n">error</span><span class="o">&gt;</span><span class="nc">Failed</span><span class="w"> </span><span class="n">to</span><span class="w"> </span><span class="n">load</span><span class="w"> </span><span class="n">chart</span><span class="o">&lt;</span><span class="o">/</span><span class="o">.</span><span class="n">error</span><span class="o">&gt;</span><span class="o">&lt;</span><span class="o">/</span><span class="ss">:failed</span><span class="o">&gt;</span><span class="w">
    </span><span class="o">&lt;</span><span class="o">/</span><span class="o">.</span><span class="n">async_result</span><span class="o">&gt;</span><span class="w">

    </span><span class="o">&lt;</span><span class="n">div</span><span class="w"> </span><span class="n">id</span><span class="o">=</span><span class="s">&quot;line_stack&quot;</span><span class="w"> </span><span class="n">phx</span><span class="o">-</span><span class="n">hook</span><span class="o">=</span><span class="s">&quot;Chart&quot;</span><span class="w"> </span><span class="n">class</span><span class="o">=</span><span class="s">&quot;w-full h-[20rem]&quot;</span><span class="w"> </span><span class="n">phx</span><span class="o">-</span><span class="n">update</span><span class="o">=</span><span class="s">&quot;ignore&quot;</span><span class="w"> </span><span class="o">/</span><span class="o">&gt;</span><span class="w">
  </span><span class="o">&lt;</span><span class="o">/</span><span class="n">div</span><span class="o">&gt;</span><span class="w">

  </span><span class="o">&lt;</span><span class="n">div</span><span class="w"> </span><span class="n">class</span><span class="o">=</span><span class="s">&quot;grid w-full grid-cols-1 gap-4 xl:grid-cols-2&quot;</span><span class="o">&gt;</span><span class="w">
    </span><span class="o">&lt;</span><span class="n">div</span><span class="w"> </span><span class="n">class</span><span class="o">=</span><span class="s">&quot;grid grid-row gap-4 p-4&quot;</span><span class="o">&gt;</span><span class="w">
      </span><span class="o">&lt;</span><span class="o">.</span><span class="n">header</span><span class="o">&gt;</span><span class="w">
        </span><span class="nc">Gauge</span><span class="w"> </span><span class="n">multi</span><span class="w"> </span><span class="n">title</span><span class="w">
      </span><span class="o">&lt;</span><span class="o">/</span><span class="o">.</span><span class="n">header</span><span class="o">&gt;</span><span class="w">

      </span><span class="o">&lt;</span><span class="o">.</span><span class="n">async_result</span><span class="w"> </span><span class="n">assign</span><span class="o">=</span><span class="p" data-group-id="5401799441-10">{</span><span class="na">@gauge_multi_title</span><span class="p" data-group-id="5401799441-10">}</span><span class="o">&gt;</span><span class="w">
        </span><span class="o">&lt;</span><span class="ss">:loading</span><span class="o">&gt;</span><span class="nc">Loading</span><span class="o">..</span><span class="o">&lt;</span><span class="o">/</span><span class="ss">:loading</span><span class="o">&gt;</span><span class="w">
        </span><span class="o">&lt;</span><span class="ss">:failed</span><span class="o">&gt;</span><span class="o">&lt;</span><span class="o">.</span><span class="n">error</span><span class="o">&gt;</span><span class="nc">Failed</span><span class="w"> </span><span class="n">to</span><span class="w"> </span><span class="n">load</span><span class="w"> </span><span class="n">chart</span><span class="o">&lt;</span><span class="o">/</span><span class="o">.</span><span class="n">error</span><span class="o">&gt;</span><span class="o">&lt;</span><span class="o">/</span><span class="ss">:failed</span><span class="o">&gt;</span><span class="w">
      </span><span class="o">&lt;</span><span class="o">/</span><span class="o">.</span><span class="n">async_result</span><span class="o">&gt;</span><span class="w">

      </span><span class="o">&lt;</span><span class="n">div</span><span class="w"> </span><span class="n">id</span><span class="o">=</span><span class="s">&quot;guage_multi_title&quot;</span><span class="w"> </span><span class="n">phx</span><span class="o">-</span><span class="n">hook</span><span class="o">=</span><span class="s">&quot;Chart&quot;</span><span class="w"> </span><span class="n">class</span><span class="o">=</span><span class="s">&quot;w-full h-[20rem]&quot;</span><span class="w"> </span><span class="n">phx</span><span class="o">-</span><span class="n">update</span><span class="o">=</span><span class="s">&quot;ignore&quot;</span><span class="w"> </span><span class="o">/</span><span class="o">&gt;</span><span class="w">
    </span><span class="o">&lt;</span><span class="o">/</span><span class="n">div</span><span class="o">&gt;</span><span class="w">

    </span><span class="o">&lt;</span><span class="n">div</span><span class="w"> </span><span class="n">class</span><span class="o">=</span><span class="s">&quot;grid grid-row gap-4 p-4&quot;</span><span class="o">&gt;</span><span class="w">
      </span><span class="o">&lt;</span><span class="o">.</span><span class="n">header</span><span class="o">&gt;</span><span class="w">
        </span><span class="nc">Processes</span><span class="w">
      </span><span class="o">&lt;</span><span class="o">/</span><span class="o">.</span><span class="n">header</span><span class="o">&gt;</span><span class="w">

      </span><span class="o">&lt;</span><span class="o">.</span><span class="n">async_result</span><span class="w"> </span><span class="n">assign</span><span class="o">=</span><span class="p" data-group-id="5401799441-11">{</span><span class="na">@process_gauge</span><span class="p" data-group-id="5401799441-11">}</span><span class="o">&gt;</span><span class="w">
        </span><span class="o">&lt;</span><span class="ss">:loading</span><span class="o">&gt;</span><span class="nc">Loading</span><span class="n">...</span><span class="o">&lt;</span><span class="o">/</span><span class="ss">:loading</span><span class="o">&gt;</span><span class="w">
        </span><span class="o">&lt;</span><span class="ss">:failed</span><span class="o">&gt;</span><span class="o">&lt;</span><span class="o">.</span><span class="n">error</span><span class="o">&gt;</span><span class="nc">Failed</span><span class="w"> </span><span class="n">to</span><span class="w"> </span><span class="n">load</span><span class="w"> </span><span class="n">chart</span><span class="o">&lt;</span><span class="o">/</span><span class="o">.</span><span class="n">error</span><span class="o">&gt;</span><span class="o">&lt;</span><span class="o">/</span><span class="ss">:failed</span><span class="o">&gt;</span><span class="w">
      </span><span class="o">&lt;</span><span class="o">/</span><span class="o">.</span><span class="n">async_result</span><span class="o">&gt;</span><span class="w">

      </span><span class="o">&lt;</span><span class="n">div</span><span class="w"> </span><span class="n">id</span><span class="o">=</span><span class="s">&quot;process_gauge&quot;</span><span class="w"> </span><span class="n">phx</span><span class="o">-</span><span class="n">hook</span><span class="o">=</span><span class="s">&quot;Chart&quot;</span><span class="w"> </span><span class="n">class</span><span class="o">=</span><span class="s">&quot;w-full h-[20rem]&quot;</span><span class="w"> </span><span class="n">phx</span><span class="o">-</span><span class="n">update</span><span class="o">=</span><span class="s">&quot;ignore&quot;</span><span class="w"> </span><span class="o">/</span><span class="o">&gt;</span><span class="w">
    </span><span class="o">&lt;</span><span class="o">/</span><span class="n">div</span><span class="o">&gt;</span><span class="w">
  </span><span class="o">&lt;</span><span class="o">/</span><span class="n">div</span><span class="o">&gt;</span><span class="w">
</span><span class="o">&lt;</span><span class="o">/</span><span class="n">div</span><span class="o">&gt;</span></code></pre>
<p>
Now we have live updating charts managed entirely by the LiveView module!</p>
<p>
Next you’ll want to produce <a href="/posts/normalize-chart-data-using-ecto-postgresql">good chart data</a>.</p>
<details><summary class="cursor-pointer">
Tests for <code class="inline">MyAppWeb.DashboardLive</code>.
</summary>
<p>
The test for the LiveView is straightforward. We just need to assert the async load and trigger the button.</p>
<pre><code class="makeup elixir"><span class="kd">defmodule</span><span class="w"> </span><span class="nc">MyAppWeb.DashboardLiveTest</span><span class="w"> </span><span class="k" data-group-id="0328496180-1">do</span><span class="w">
  </span><span class="kn">use</span><span class="w"> </span><span class="nc">MyAppWeb.ConnCase</span><span class="w">

  </span><span class="kn">import</span><span class="w"> </span><span class="nc">Phoenix.LiveViewTest</span><span class="w">

  </span><span class="c1"># You&#39;ll want to seed the database for chart data from the database to catch</span><span class="w">
  </span><span class="c1"># any issues with transforming the data to ECharts options:</span><span class="w">
  </span><span class="c1"># </span><span class="w">
  </span><span class="c1">#   setup :seed_data</span><span class="w">
  </span><span class="c1">#</span><span class="w">
  </span><span class="c1">#   defp seed_data(_) do</span><span class="w">
  </span><span class="c1">#     for n &lt;- 1..100, do: record_fixture(%{value: n})</span><span class="w">
  </span><span class="c1">#</span><span class="w">
  </span><span class="c1">#     :ok</span><span class="w">
  </span><span class="c1">#   end</span><span class="w">

  </span><span class="n">test</span><span class="w"> </span><span class="s">&quot;renders&quot;</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="0328496180-2">%{</span><span class="ss">conn</span><span class="p">:</span><span class="w"> </span><span class="n">conn</span><span class="p" data-group-id="0328496180-2">}</span><span class="w"> </span><span class="k" data-group-id="0328496180-3">do</span><span class="w">
    </span><span class="p" data-group-id="0328496180-4">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="n">view</span><span class="p">,</span><span class="w"> </span><span class="n">html</span><span class="p" data-group-id="0328496180-4">}</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">live</span><span class="p" data-group-id="0328496180-5">(</span><span class="n">conn</span><span class="p">,</span><span class="w"> </span><span class="sx">~p&quot;/dashboard&quot;</span><span class="p" data-group-id="0328496180-5">)</span><span class="w">

    </span><span class="n">assert</span><span class="w"> </span><span class="n">html</span><span class="w"> </span><span class="o">=~</span><span class="w"> </span><span class="s">&quot;Loading...&quot;</span><span class="w">
    </span><span class="n">refute</span><span class="w"> </span><span class="n">render_async</span><span class="p" data-group-id="0328496180-6">(</span><span class="n">view</span><span class="p" data-group-id="0328496180-6">)</span><span class="w"> </span><span class="o">=~</span><span class="w"> </span><span class="s">&quot;Loading...&quot;</span><span class="w">

    </span><span class="n">assert</span><span class="w"> </span><span class="n">view</span><span class="w"> </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">element</span><span class="p" data-group-id="0328496180-7">(</span><span class="s">&quot;button&quot;</span><span class="p">,</span><span class="w"> </span><span class="s">&quot;Single&quot;</span><span class="p" data-group-id="0328496180-7">)</span><span class="w"> </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">render_click</span><span class="p" data-group-id="0328496180-8">(</span><span class="p" data-group-id="0328496180-8">)</span><span class="w"> </span><span class="o">=~</span><span class="w"> </span><span class="s">&quot;Rolling&quot;</span><span class="w">
  </span><span class="k" data-group-id="0328496180-3">end</span><span class="w">
</span><span class="k" data-group-id="0328496180-1">end</span></code></pre>
</details>

      ]]>
    </content>
  </entry>
  
  <entry>
    <title>Normalize chart data using Ecto and PostgreSQL</title>
    <link href="https://danschultzer.com/posts/normalize-chart-data-using-ecto-postgresql" />
    <id>https://danschultzer.com/posts/normalize-chart-data-using-ecto-postgresql</id>
    <updated>2025-02-15T00:00:00Z</updated>
    <summary>How to produce good chart data using Ecto and PostgreSQL</summary>
    <content type="html">
      <![CDATA[
        <p>
If you don’t yet have charts in your Phoenix app, you may want to <a href="/posts/echarts-phoenix-liveview">set up ECharts with Phoenix LiveView</a>.</p>
<p>
Producing good datasets for charts is essential. A graph is only as good as the data it uses. Here is a collection of helpful snippets I have used to chart data.</p>
<h2>
Finding the right step size</h2>
<h3>
Numerical</h3>
<p>
For numerical steps, you can do a simple calculation with <code class="inline">(end - start) / steps</code> to find the step size, but unless you have fixed <code class="inline">start</code> and <code class="inline">end</code> values, you will end up with odd numbers (e.g. <code class="inline">167.3998</code>).</p>
<p>
Instead, we want to snap the steps to <code class="inline">1</code>, <code class="inline">2.5</code>, <code class="inline">5</code> (scaled to the proper size) so it reads well for the end-user. We’re using <a href="https://hex.pm/packages/decimal"><code class="inline">Decimal</code></a> to calculate the step size below:</p>
<pre><code class="makeup elixir"><span class="kd">defmodule</span><span class="w"> </span><span class="nc">MyApp.ChartUtils</span><span class="w"> </span><span class="k" data-group-id="2321343224-1">do</span><span class="w">
  </span><span class="na">@moduledoc</span><span class="w"> </span><span class="s">&quot;&quot;&quot;
  Utility functions for charts.
  &quot;&quot;&quot;</span><span class="w">

  </span><span class="na">@doc</span><span class="w"> </span><span class="s">&quot;&quot;&quot;
  Finds an appropriate numerical step size.
  &quot;&quot;&quot;</span><span class="w">
  </span><span class="kd">def</span><span class="w"> </span><span class="nf">step_size</span><span class="p" data-group-id="2321343224-2">(</span><span class="n">min</span><span class="p">,</span><span class="w"> </span><span class="n">max</span><span class="p">,</span><span class="w"> </span><span class="n">steps</span><span class="w"> </span><span class="o">\\</span><span class="w"> </span><span class="mi">100</span><span class="p" data-group-id="2321343224-2">)</span><span class="w"> </span><span class="k" data-group-id="2321343224-3">do</span><span class="w">
    </span><span class="n">step_size</span><span class="w"> </span><span class="o">=</span><span class="w">
      </span><span class="n">max</span><span class="w">
      </span><span class="o">|&gt;</span><span class="w"> </span><span class="nc">Decimal</span><span class="o">.</span><span class="n">sub</span><span class="p" data-group-id="2321343224-4">(</span><span class="n">min</span><span class="p" data-group-id="2321343224-4">)</span><span class="w">
      </span><span class="o">|&gt;</span><span class="w"> </span><span class="nc">Decimal</span><span class="o">.</span><span class="n">div</span><span class="p" data-group-id="2321343224-5">(</span><span class="n">steps</span><span class="p" data-group-id="2321343224-5">)</span><span class="w">
      </span><span class="c1"># We don&#39;t want a step size lower than `1` adjusted to the lowest value exponent</span><span class="w">
      </span><span class="o">|&gt;</span><span class="w"> </span><span class="nc">Decimal</span><span class="o">.</span><span class="n">max</span><span class="p" data-group-id="2321343224-6">(</span><span class="nc">Decimal</span><span class="o">.</span><span class="n">new</span><span class="p" data-group-id="2321343224-7">(</span><span class="mi">1</span><span class="p">,</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span><span class="w"> </span><span class="n">min</span><span class="o">.</span><span class="n">exp</span><span class="p" data-group-id="2321343224-7">)</span><span class="p" data-group-id="2321343224-6">)</span><span class="w">

    </span><span class="c1"># Calculating the exponent we need for our step size</span><span class="w">
    </span><span class="n">pow10</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">Integer</span><span class="o">.</span><span class="n">pow</span><span class="p" data-group-id="2321343224-8">(</span><span class="mi">10</span><span class="p">,</span><span class="w"> </span><span class="n">length</span><span class="p" data-group-id="2321343224-9">(</span><span class="nc">Integer</span><span class="o">.</span><span class="n">digits</span><span class="p" data-group-id="2321343224-10">(</span><span class="n">step_size</span><span class="o">.</span><span class="n">coef</span><span class="p" data-group-id="2321343224-10">)</span><span class="p" data-group-id="2321343224-9">)</span><span class="w"> </span><span class="o">-</span><span class="w"> </span><span class="mi">1</span><span class="p" data-group-id="2321343224-8">)</span><span class="w">
    </span><span class="n">exp</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="o">-</span><span class="w"> </span><span class="mi">1</span><span class="w"> </span><span class="o">-</span><span class="w"> </span><span class="nc">Decimal</span><span class="o">.</span><span class="n">scale</span><span class="p" data-group-id="2321343224-11">(</span><span class="n">step_size</span><span class="p" data-group-id="2321343224-11">)</span><span class="w">

    </span><span class="c1"># Find the right fit in our list of step sizes to snap to</span><span class="w">
    </span><span class="p" data-group-id="2321343224-12">[</span><span class="w">
      </span><span class="nc">Decimal</span><span class="o">.</span><span class="n">new</span><span class="p" data-group-id="2321343224-13">(</span><span class="mi">1</span><span class="p">,</span><span class="w"> </span><span class="mi">10</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="n">pow10</span><span class="p">,</span><span class="w"> </span><span class="n">exp</span><span class="p" data-group-id="2321343224-13">)</span><span class="p">,</span><span class="w">
      </span><span class="nc">Decimal</span><span class="o">.</span><span class="n">new</span><span class="p" data-group-id="2321343224-14">(</span><span class="mi">1</span><span class="p">,</span><span class="w"> </span><span class="mi">25</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="n">pow10</span><span class="p">,</span><span class="w"> </span><span class="n">exp</span><span class="p" data-group-id="2321343224-14">)</span><span class="p">,</span><span class="w">
      </span><span class="nc">Decimal</span><span class="o">.</span><span class="n">new</span><span class="p" data-group-id="2321343224-15">(</span><span class="mi">1</span><span class="p">,</span><span class="w"> </span><span class="mi">50</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="n">pow10</span><span class="p">,</span><span class="w"> </span><span class="n">exp</span><span class="p" data-group-id="2321343224-15">)</span><span class="p">,</span><span class="w">
      </span><span class="nc">Decimal</span><span class="o">.</span><span class="n">new</span><span class="p" data-group-id="2321343224-16">(</span><span class="mi">1</span><span class="p">,</span><span class="w"> </span><span class="mi">100</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="n">pow10</span><span class="p">,</span><span class="w"> </span><span class="n">exp</span><span class="p" data-group-id="2321343224-16">)</span><span class="w">
    </span><span class="p" data-group-id="2321343224-12">]</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="nc">Enum</span><span class="o">.</span><span class="n">find</span><span class="p" data-group-id="2321343224-17">(</span><span class="o">&amp;</span><span class="nc">Decimal</span><span class="o">.</span><span class="n">lte?</span><span class="p" data-group-id="2321343224-18">(</span><span class="n">step_size</span><span class="p">,</span><span class="w"> </span><span class="ni">&amp;1</span><span class="p" data-group-id="2321343224-18">)</span><span class="p" data-group-id="2321343224-17">)</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="nc">Decimal</span><span class="o">.</span><span class="n">normalize</span><span class="p" data-group-id="2321343224-19">(</span><span class="p" data-group-id="2321343224-19">)</span><span class="w">
  </span><span class="k" data-group-id="2321343224-3">end</span><span class="w">
</span><span class="k" data-group-id="2321343224-1">end</span></code></pre>
<details><summary class="cursor-pointer">
Tests for <code class="inline">step_size/3</code>.
</summary>
<pre><code class="makeup elixir"><span class="kd">defmodule</span><span class="w"> </span><span class="nc">MyApp.ChartUtilsTest</span><span class="w"> </span><span class="k" data-group-id="5609712834-1">do</span><span class="w">
  </span><span class="kn">use</span><span class="w"> </span><span class="nc">MyApp.DataCase</span><span class="w">

  </span><span class="kn">alias</span><span class="w"> </span><span class="nc">MyApp.ChartUtils</span><span class="w">

  </span><span class="n">test</span><span class="w"> </span><span class="s">&quot;step_size/3&quot;</span><span class="w"> </span><span class="k" data-group-id="5609712834-2">do</span><span class="w">
    </span><span class="n">assert</span><span class="w"> </span><span class="nc">ChartUtils</span><span class="o">.</span><span class="n">step_size</span><span class="p" data-group-id="5609712834-3">(</span><span class="nc">Decimal</span><span class="o">.</span><span class="n">new</span><span class="p" data-group-id="5609712834-4">(</span><span class="mi">1</span><span class="p" data-group-id="5609712834-4">)</span><span class="p">,</span><span class="w"> </span><span class="nc">Decimal</span><span class="o">.</span><span class="n">new</span><span class="p" data-group-id="5609712834-5">(</span><span class="mi">100</span><span class="p" data-group-id="5609712834-5">)</span><span class="p" data-group-id="5609712834-3">)</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="nc">Decimal</span><span class="o">.</span><span class="n">new</span><span class="p" data-group-id="5609712834-6">(</span><span class="s">&quot;1&quot;</span><span class="p" data-group-id="5609712834-6">)</span><span class="w">
    </span><span class="n">assert</span><span class="w"> </span><span class="nc">ChartUtils</span><span class="o">.</span><span class="n">step_size</span><span class="p" data-group-id="5609712834-7">(</span><span class="nc">Decimal</span><span class="o">.</span><span class="n">new</span><span class="p" data-group-id="5609712834-8">(</span><span class="o">-</span><span class="mi">100</span><span class="p" data-group-id="5609712834-8">)</span><span class="p">,</span><span class="w"> </span><span class="nc">Decimal</span><span class="o">.</span><span class="n">new</span><span class="p" data-group-id="5609712834-9">(</span><span class="mi">1</span><span class="p" data-group-id="5609712834-9">)</span><span class="p" data-group-id="5609712834-7">)</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="nc">Decimal</span><span class="o">.</span><span class="n">new</span><span class="p" data-group-id="5609712834-10">(</span><span class="s">&quot;2.5&quot;</span><span class="p" data-group-id="5609712834-10">)</span><span class="w">
    </span><span class="n">assert</span><span class="w"> </span><span class="nc">ChartUtils</span><span class="o">.</span><span class="n">step_size</span><span class="p" data-group-id="5609712834-11">(</span><span class="nc">Decimal</span><span class="o">.</span><span class="n">new</span><span class="p" data-group-id="5609712834-12">(</span><span class="mi">100</span><span class="p" data-group-id="5609712834-12">)</span><span class="p">,</span><span class="w"> </span><span class="nc">Decimal</span><span class="o">.</span><span class="n">new</span><span class="p" data-group-id="5609712834-13">(</span><span class="mi">1_000</span><span class="p" data-group-id="5609712834-13">)</span><span class="p" data-group-id="5609712834-11">)</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="nc">Decimal</span><span class="o">.</span><span class="n">new</span><span class="p" data-group-id="5609712834-14">(</span><span class="s">&quot;1E+1&quot;</span><span class="p" data-group-id="5609712834-14">)</span><span class="w">
    </span><span class="n">assert</span><span class="w"> </span><span class="nc">ChartUtils</span><span class="o">.</span><span class="n">step_size</span><span class="p" data-group-id="5609712834-15">(</span><span class="nc">Decimal</span><span class="o">.</span><span class="n">new</span><span class="p" data-group-id="5609712834-16">(</span><span class="mi">100</span><span class="p" data-group-id="5609712834-16">)</span><span class="p">,</span><span class="w"> </span><span class="nc">Decimal</span><span class="o">.</span><span class="n">new</span><span class="p" data-group-id="5609712834-17">(</span><span class="mi">1_000</span><span class="p" data-group-id="5609712834-17">)</span><span class="p">,</span><span class="w"> </span><span class="mi">3</span><span class="p" data-group-id="5609712834-15">)</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="nc">Decimal</span><span class="o">.</span><span class="n">new</span><span class="p" data-group-id="5609712834-18">(</span><span class="s">&quot;5E+2&quot;</span><span class="p" data-group-id="5609712834-18">)</span><span class="w">
    </span><span class="n">assert</span><span class="w"> </span><span class="nc">ChartUtils</span><span class="o">.</span><span class="n">step_size</span><span class="p" data-group-id="5609712834-19">(</span><span class="nc">Decimal</span><span class="o">.</span><span class="n">from_float</span><span class="p" data-group-id="5609712834-20">(</span><span class="mf">331.32</span><span class="p" data-group-id="5609712834-20">)</span><span class="p">,</span><span class="w"> </span><span class="nc">Decimal</span><span class="o">.</span><span class="n">from_float</span><span class="p" data-group-id="5609712834-21">(</span><span class="mf">7_893.47</span><span class="p" data-group-id="5609712834-21">)</span><span class="p" data-group-id="5609712834-19">)</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="nc">Decimal</span><span class="o">.</span><span class="n">new</span><span class="p" data-group-id="5609712834-22">(</span><span class="s">&quot;1E+2&quot;</span><span class="p" data-group-id="5609712834-22">)</span><span class="w">
  </span><span class="k" data-group-id="5609712834-2">end</span><span class="w">
</span><span class="k" data-group-id="5609712834-1">end</span></code></pre>
</details>
<h3>
Datetime</h3>
<p>
Usually, we don’t have a fixed time frame for our graphs. Some data may have much lower or higher time ranges than others.</p>
<p>
One difference from numerical step sizes is that minutes, hours, and days are not 1:1 like the numerical values above, and there isn’t a good way to calculate it. We don’t think of time in base-10, but rather in terms of 5 minutes, half hours, daily, biweekly, quarterly, and so on.</p>
<p>
Therefore, we don’t pass in the desired number of steps but simply hardcode the breakpoints for each time interval:</p>
<pre><code class="makeup elixir"><span class="kd">defmodule</span><span class="w"> </span><span class="nc">MyApp.ChartUtils</span><span class="w"> </span><span class="k" data-group-id="7189133344-1">do</span><span class="w">
  </span><span class="c1"># ...</span><span class="w">

  </span><span class="na">@doc</span><span class="w"> </span><span class="s">&quot;&quot;&quot;
  Finds an appropriate time interval.
  &quot;&quot;&quot;</span><span class="w">
  </span><span class="kd">def</span><span class="w"> </span><span class="nf">time_interval</span><span class="p" data-group-id="7189133344-2">(</span><span class="n">begins_at</span><span class="p">,</span><span class="w"> </span><span class="n">ends_at</span><span class="w"> </span><span class="o">\\</span><span class="w"> </span><span class="no">nil</span><span class="p" data-group-id="7189133344-2">)</span><span class="w"> </span><span class="k" data-group-id="7189133344-3">do</span><span class="w">
    </span><span class="n">ends_at</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">ends_at</span><span class="w"> </span><span class="o">||</span><span class="w"> </span><span class="nc">DateTime</span><span class="o">.</span><span class="n">utc_now</span><span class="p" data-group-id="7189133344-4">(</span><span class="p" data-group-id="7189133344-4">)</span><span class="w">

    </span><span class="n">ends_at</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="nc">DateTime</span><span class="o">.</span><span class="n">diff</span><span class="p" data-group-id="7189133344-5">(</span><span class="n">begins_at</span><span class="p">,</span><span class="w"> </span><span class="ss">:millisecond</span><span class="p" data-group-id="7189133344-5">)</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">do_time_interval</span><span class="p" data-group-id="7189133344-6">(</span><span class="p" data-group-id="7189133344-6">)</span><span class="w">
  </span><span class="k" data-group-id="7189133344-3">end</span><span class="w">

  </span><span class="kd">defp</span><span class="w"> </span><span class="nf">do_time_interval</span><span class="p" data-group-id="7189133344-7">(</span><span class="n">diff</span><span class="p" data-group-id="7189133344-7">)</span><span class="w"> </span><span class="ow">when</span><span class="w"> </span><span class="n">diff</span><span class="w"> </span><span class="o">&lt;</span><span class="w"> </span><span class="k">unquote</span><span class="p" data-group-id="7189133344-8">(</span><span class="nc">:timer</span><span class="o">.</span><span class="n">minutes</span><span class="p" data-group-id="7189133344-9">(</span><span class="mi">60</span><span class="p" data-group-id="7189133344-9">)</span><span class="p" data-group-id="7189133344-8">)</span><span class="p">,</span><span class="w"> </span><span class="ss">do</span><span class="p">:</span><span class="w"> </span><span class="p" data-group-id="7189133344-10">{</span><span class="ss">:minute</span><span class="p">,</span><span class="w"> </span><span class="mi">1</span><span class="p" data-group-id="7189133344-10">}</span><span class="w">
  </span><span class="kd">defp</span><span class="w"> </span><span class="nf">do_time_interval</span><span class="p" data-group-id="7189133344-11">(</span><span class="n">diff</span><span class="p" data-group-id="7189133344-11">)</span><span class="w"> </span><span class="ow">when</span><span class="w"> </span><span class="n">diff</span><span class="w"> </span><span class="o">&lt;</span><span class="w"> </span><span class="k">unquote</span><span class="p" data-group-id="7189133344-12">(</span><span class="nc">:timer</span><span class="o">.</span><span class="n">hours</span><span class="p" data-group-id="7189133344-13">(</span><span class="mi">4</span><span class="p" data-group-id="7189133344-13">)</span><span class="p" data-group-id="7189133344-12">)</span><span class="p">,</span><span class="w"> </span><span class="ss">do</span><span class="p">:</span><span class="w"> </span><span class="p" data-group-id="7189133344-14">{</span><span class="ss">:minute</span><span class="p">,</span><span class="w"> </span><span class="mi">5</span><span class="p" data-group-id="7189133344-14">}</span><span class="w">
  </span><span class="kd">defp</span><span class="w"> </span><span class="nf">do_time_interval</span><span class="p" data-group-id="7189133344-15">(</span><span class="n">diff</span><span class="p" data-group-id="7189133344-15">)</span><span class="w"> </span><span class="ow">when</span><span class="w"> </span><span class="n">diff</span><span class="w"> </span><span class="o">&lt;</span><span class="w"> </span><span class="k">unquote</span><span class="p" data-group-id="7189133344-16">(</span><span class="nc">:timer</span><span class="o">.</span><span class="n">hours</span><span class="p" data-group-id="7189133344-17">(</span><span class="mi">12</span><span class="p" data-group-id="7189133344-17">)</span><span class="p" data-group-id="7189133344-16">)</span><span class="p">,</span><span class="w"> </span><span class="ss">do</span><span class="p">:</span><span class="w"> </span><span class="p" data-group-id="7189133344-18">{</span><span class="ss">:minute</span><span class="p">,</span><span class="w"> </span><span class="mi">30</span><span class="p" data-group-id="7189133344-18">}</span><span class="w">
  </span><span class="kd">defp</span><span class="w"> </span><span class="nf">do_time_interval</span><span class="p" data-group-id="7189133344-19">(</span><span class="n">diff</span><span class="p" data-group-id="7189133344-19">)</span><span class="w"> </span><span class="ow">when</span><span class="w"> </span><span class="n">diff</span><span class="w"> </span><span class="o">&lt;</span><span class="w"> </span><span class="k">unquote</span><span class="p" data-group-id="7189133344-20">(</span><span class="nc">:timer</span><span class="o">.</span><span class="n">hours</span><span class="p" data-group-id="7189133344-21">(</span><span class="mi">24</span><span class="p" data-group-id="7189133344-21">)</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="mi">2</span><span class="p" data-group-id="7189133344-20">)</span><span class="p">,</span><span class="w"> </span><span class="ss">do</span><span class="p">:</span><span class="w"> </span><span class="p" data-group-id="7189133344-22">{</span><span class="ss">:hour</span><span class="p">,</span><span class="w"> </span><span class="mi">1</span><span class="p" data-group-id="7189133344-22">}</span><span class="w">
  </span><span class="kd">defp</span><span class="w"> </span><span class="nf">do_time_interval</span><span class="p" data-group-id="7189133344-23">(</span><span class="n">diff</span><span class="p" data-group-id="7189133344-23">)</span><span class="w"> </span><span class="ow">when</span><span class="w"> </span><span class="n">diff</span><span class="w"> </span><span class="o">&lt;</span><span class="w"> </span><span class="k">unquote</span><span class="p" data-group-id="7189133344-24">(</span><span class="nc">:timer</span><span class="o">.</span><span class="n">hours</span><span class="p" data-group-id="7189133344-25">(</span><span class="mi">24</span><span class="p" data-group-id="7189133344-25">)</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="mi">3</span><span class="p" data-group-id="7189133344-24">)</span><span class="p">,</span><span class="w"> </span><span class="ss">do</span><span class="p">:</span><span class="w"> </span><span class="p" data-group-id="7189133344-26">{</span><span class="ss">:hour</span><span class="p">,</span><span class="w"> </span><span class="mi">6</span><span class="p" data-group-id="7189133344-26">}</span><span class="w">
  </span><span class="kd">defp</span><span class="w"> </span><span class="nf">do_time_interval</span><span class="p" data-group-id="7189133344-27">(</span><span class="c">_diff</span><span class="p" data-group-id="7189133344-27">)</span><span class="p">,</span><span class="w"> </span><span class="ss">do</span><span class="p">:</span><span class="w"> </span><span class="p" data-group-id="7189133344-28">{</span><span class="ss">:day</span><span class="p">,</span><span class="w"> </span><span class="mi">1</span><span class="p" data-group-id="7189133344-28">}</span><span class="w">
</span><span class="k" data-group-id="7189133344-1">end</span></code></pre>
<p>
You will need to adjust it to your specific use case, as these time intervals are context-dependent. The above example is used for running events where, after a few days, you want to see daily intervals even though it only produces three data points.</p>
<p>
With the above, we can now use the interval for grouping in our Ecto query:</p>
<pre><code class="makeup elixir"><span class="kd">defmodule</span><span class="w"> </span><span class="nc">MyApp.Records</span><span class="w"> </span><span class="k" data-group-id="1800746085-1">do</span><span class="w">
  </span><span class="kn">alias</span><span class="w"> </span><span class="nc">MyApp</span><span class="o">.</span><span class="p" data-group-id="1800746085-2">{</span><span class="nc">Records.Record</span><span class="p">,</span><span class="w"> </span><span class="nc">Repo</span><span class="p" data-group-id="1800746085-2">}</span><span class="w">

  </span><span class="kn">import</span><span class="w"> </span><span class="nc">Ecto.Query</span><span class="w">

  </span><span class="kd">def</span><span class="w"> </span><span class="nf">list_record_counts_over_time</span><span class="p" data-group-id="1800746085-3">(</span><span class="n">time_interval</span><span class="p" data-group-id="1800746085-3">)</span><span class="w"> </span><span class="k" data-group-id="1800746085-4">do</span><span class="w">
    </span><span class="nc">Record</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">group_by</span><span class="p" data-group-id="1800746085-5">(</span><span class="p" data-group-id="1800746085-6">[</span><span class="n">r</span><span class="p" data-group-id="1800746085-6">]</span><span class="p">,</span><span class="w"> </span><span class="n">fragment</span><span class="p" data-group-id="1800746085-7">(</span><span class="s">&quot;timestamp&quot;</span><span class="p" data-group-id="1800746085-7">)</span><span class="p" data-group-id="1800746085-5">)</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">select</span><span class="p" data-group-id="1800746085-8">(</span><span class="p" data-group-id="1800746085-9">[</span><span class="n">r</span><span class="p" data-group-id="1800746085-9">]</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="1800746085-10">%{</span><span class="w">
      </span><span class="ss">timestamp</span><span class="p">:</span><span class="w"> </span><span class="n">fragment</span><span class="p" data-group-id="1800746085-11">(</span><span class="s">&quot;date_bin(?, ?, &#39;epoch&#39;) as timestamp&quot;</span><span class="p">,</span><span class="w"> </span><span class="o">^</span><span class="n">to_interval</span><span class="p" data-group-id="1800746085-12">(</span><span class="n">time_interval</span><span class="p" data-group-id="1800746085-12">)</span><span class="p">,</span><span class="w"> </span><span class="n">r</span><span class="o">.</span><span class="n">inserted_at</span><span class="p" data-group-id="1800746085-11">)</span><span class="p">,</span><span class="w">
      </span><span class="ss">count</span><span class="p">:</span><span class="w"> </span><span class="n">count</span><span class="p" data-group-id="1800746085-13">(</span><span class="n">r</span><span class="o">.</span><span class="n">id</span><span class="p" data-group-id="1800746085-13">)</span><span class="w">
    </span><span class="p" data-group-id="1800746085-10">}</span><span class="p" data-group-id="1800746085-8">)</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">order_by</span><span class="p" data-group-id="1800746085-14">(</span><span class="p" data-group-id="1800746085-15">[</span><span class="n">r</span><span class="p" data-group-id="1800746085-15">]</span><span class="p">,</span><span class="w"> </span><span class="ss">asc</span><span class="p">:</span><span class="w"> </span><span class="n">fragment</span><span class="p" data-group-id="1800746085-16">(</span><span class="s">&quot;timestamp&quot;</span><span class="p" data-group-id="1800746085-16">)</span><span class="p" data-group-id="1800746085-14">)</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="nc">Repo</span><span class="o">.</span><span class="n">all</span><span class="p" data-group-id="1800746085-17">(</span><span class="p" data-group-id="1800746085-17">)</span><span class="w">
  </span><span class="k" data-group-id="1800746085-4">end</span><span class="w">

  </span><span class="kd">defp</span><span class="w"> </span><span class="nf">to_interval</span><span class="p" data-group-id="1800746085-18">(</span><span class="p" data-group-id="1800746085-19">{</span><span class="ss">:minute</span><span class="p">,</span><span class="w"> </span><span class="n">n</span><span class="p" data-group-id="1800746085-19">}</span><span class="p" data-group-id="1800746085-18">)</span><span class="p">,</span><span class="w"> </span><span class="ss">do</span><span class="p">:</span><span class="w"> </span><span class="p" data-group-id="1800746085-20">%</span><span class="nc" data-group-id="1800746085-20">Postgrex.Interval</span><span class="p" data-group-id="1800746085-20">{</span><span class="ss">secs</span><span class="p">:</span><span class="w"> </span><span class="n">n</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="mi">60</span><span class="p" data-group-id="1800746085-20">}</span><span class="w">
  </span><span class="kd">defp</span><span class="w"> </span><span class="nf">to_interval</span><span class="p" data-group-id="1800746085-21">(</span><span class="p" data-group-id="1800746085-22">{</span><span class="ss">:hour</span><span class="p">,</span><span class="w"> </span><span class="n">n</span><span class="p" data-group-id="1800746085-22">}</span><span class="p" data-group-id="1800746085-21">)</span><span class="p">,</span><span class="w"> </span><span class="ss">do</span><span class="p">:</span><span class="w"> </span><span class="p" data-group-id="1800746085-23">%</span><span class="nc" data-group-id="1800746085-23">Postgrex.Interval</span><span class="p" data-group-id="1800746085-23">{</span><span class="ss">secs</span><span class="p">:</span><span class="w"> </span><span class="n">n</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="mi">60</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="mi">60</span><span class="p" data-group-id="1800746085-23">}</span><span class="w">
  </span><span class="kd">defp</span><span class="w"> </span><span class="nf">to_interval</span><span class="p" data-group-id="1800746085-24">(</span><span class="p" data-group-id="1800746085-25">{</span><span class="ss">:day</span><span class="p">,</span><span class="w"> </span><span class="n">n</span><span class="p" data-group-id="1800746085-25">}</span><span class="p" data-group-id="1800746085-24">)</span><span class="p">,</span><span class="w"> </span><span class="ss">do</span><span class="p">:</span><span class="w"> </span><span class="p" data-group-id="1800746085-26">%</span><span class="nc" data-group-id="1800746085-26">Postgrex.Interval</span><span class="p" data-group-id="1800746085-26">{</span><span class="ss">days</span><span class="p">:</span><span class="w"> </span><span class="n">n</span><span class="p" data-group-id="1800746085-26">}</span><span class="w">
</span><span class="k" data-group-id="1800746085-1">end</span></code></pre>
<details><summary class="cursor-pointer">
Tests for <code class="inline">time_interval/2</code> and <code class="inline">list_record_counts_over_time/1</code>.
</summary>
<pre><code class="makeup elixir"><span class="kd">defmodule</span><span class="w"> </span><span class="nc">MyApp.ChartUtilsTest</span><span class="w"> </span><span class="k" data-group-id="8099299047-1">do</span><span class="w">
  </span><span class="c1"># ...</span><span class="w">

  </span><span class="n">test</span><span class="w"> </span><span class="s">&quot;time_interval/2&quot;</span><span class="w"> </span><span class="k" data-group-id="8099299047-2">do</span><span class="w">
    </span><span class="n">now</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">DateTime</span><span class="o">.</span><span class="n">utc_now</span><span class="p" data-group-id="8099299047-3">(</span><span class="p" data-group-id="8099299047-3">)</span><span class="w">

    </span><span class="n">assert</span><span class="w"> </span><span class="nc">ChartUtils</span><span class="o">.</span><span class="n">time_interval</span><span class="p" data-group-id="8099299047-4">(</span><span class="n">now</span><span class="p" data-group-id="8099299047-4">)</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="p" data-group-id="8099299047-5">{</span><span class="ss">:minute</span><span class="p">,</span><span class="w"> </span><span class="mi">1</span><span class="p" data-group-id="8099299047-5">}</span><span class="w">
    </span><span class="n">assert</span><span class="w"> </span><span class="nc">ChartUtils</span><span class="o">.</span><span class="n">time_interval</span><span class="p" data-group-id="8099299047-6">(</span><span class="n">now</span><span class="p">,</span><span class="w"> </span><span class="nc">DateTime</span><span class="o">.</span><span class="n">add</span><span class="p" data-group-id="8099299047-7">(</span><span class="n">now</span><span class="p">,</span><span class="w"> </span><span class="o">-</span><span class="mi">1</span><span class="p">,</span><span class="w"> </span><span class="ss">:hour</span><span class="p" data-group-id="8099299047-7">)</span><span class="p" data-group-id="8099299047-6">)</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="p" data-group-id="8099299047-8">{</span><span class="ss">:minute</span><span class="p">,</span><span class="w"> </span><span class="mi">1</span><span class="p" data-group-id="8099299047-8">}</span><span class="w">
    </span><span class="n">assert</span><span class="w"> </span><span class="nc">ChartUtils</span><span class="o">.</span><span class="n">time_interval</span><span class="p" data-group-id="8099299047-9">(</span><span class="nc">DateTime</span><span class="o">.</span><span class="n">add</span><span class="p" data-group-id="8099299047-10">(</span><span class="n">now</span><span class="p">,</span><span class="w"> </span><span class="o">-</span><span class="mi">1</span><span class="p">,</span><span class="w"> </span><span class="ss">:hour</span><span class="p" data-group-id="8099299047-10">)</span><span class="p" data-group-id="8099299047-9">)</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="p" data-group-id="8099299047-11">{</span><span class="ss">:minute</span><span class="p">,</span><span class="w"> </span><span class="mi">5</span><span class="p" data-group-id="8099299047-11">}</span><span class="w">
    </span><span class="n">assert</span><span class="w"> </span><span class="nc">ChartUtils</span><span class="o">.</span><span class="n">time_interval</span><span class="p" data-group-id="8099299047-12">(</span><span class="nc">DateTime</span><span class="o">.</span><span class="n">add</span><span class="p" data-group-id="8099299047-13">(</span><span class="n">now</span><span class="p">,</span><span class="w"> </span><span class="o">-</span><span class="mi">365</span><span class="p">,</span><span class="w"> </span><span class="ss">:day</span><span class="p" data-group-id="8099299047-13">)</span><span class="p" data-group-id="8099299047-12">)</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="p" data-group-id="8099299047-14">{</span><span class="ss">:day</span><span class="p">,</span><span class="w"> </span><span class="mi">1</span><span class="p" data-group-id="8099299047-14">}</span><span class="w">
  </span><span class="k" data-group-id="8099299047-2">end</span><span class="w">
</span><span class="k" data-group-id="8099299047-1">end</span></code></pre>
<pre><code class="makeup elixir"><span class="kd">defmodule</span><span class="w"> </span><span class="nc">MyApp.RecordsTest</span><span class="w"> </span><span class="k" data-group-id="5483807206-1">do</span><span class="w">
  </span><span class="kn">use</span><span class="w"> </span><span class="nc">MyApp.DataCase</span><span class="w">

  </span><span class="kn">alias</span><span class="w"> </span><span class="nc">MyApp</span><span class="o">.</span><span class="p" data-group-id="5483807206-2">{</span><span class="nc">Records</span><span class="p">,</span><span class="w"> </span><span class="nc">Records.Record</span><span class="p">,</span><span class="w"> </span><span class="nc">Repo</span><span class="p" data-group-id="5483807206-2">}</span><span class="w">

  </span><span class="n">test</span><span class="w"> </span><span class="s">&quot;list_record_counts_over_time/1&quot;</span><span class="w"> </span><span class="k" data-group-id="5483807206-3">do</span><span class="w">
    </span><span class="n">beginning_of_day</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p" data-group-id="5483807206-4">%{</span><span class="nc">DateTime</span><span class="o">.</span><span class="n">utc_now</span><span class="p" data-group-id="5483807206-5">(</span><span class="p" data-group-id="5483807206-5">)</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="ss">hour</span><span class="p">:</span><span class="w"> </span><span class="mi">0</span><span class="p">,</span><span class="w"> </span><span class="ss">minute</span><span class="p">:</span><span class="w"> </span><span class="mi">0</span><span class="p">,</span><span class="w"> </span><span class="ss">second</span><span class="p">:</span><span class="w"> </span><span class="mi">0</span><span class="p">,</span><span class="w"> </span><span class="ss">microsecond</span><span class="p">:</span><span class="w"> </span><span class="p" data-group-id="5483807206-6">{</span><span class="mi">0</span><span class="p">,</span><span class="w"> </span><span class="mi">6</span><span class="p" data-group-id="5483807206-6">}</span><span class="p" data-group-id="5483807206-4">}</span><span class="w">

    </span><span class="n">record_fixture</span><span class="p" data-group-id="5483807206-7">(</span><span class="p" data-group-id="5483807206-8">%{</span><span class="ss">inserted_at</span><span class="p">:</span><span class="w"> </span><span class="nc">DateTime</span><span class="o">.</span><span class="n">add</span><span class="p" data-group-id="5483807206-9">(</span><span class="n">beginning_of_day</span><span class="p">,</span><span class="w"> </span><span class="o">-</span><span class="mi">2</span><span class="p">,</span><span class="w"> </span><span class="ss">:day</span><span class="p" data-group-id="5483807206-9">)</span><span class="p" data-group-id="5483807206-8">}</span><span class="p" data-group-id="5483807206-7">)</span><span class="w">
    </span><span class="n">record_fixture</span><span class="p" data-group-id="5483807206-10">(</span><span class="p" data-group-id="5483807206-11">%{</span><span class="ss">inserted_at</span><span class="p">:</span><span class="w"> </span><span class="nc">DateTime</span><span class="o">.</span><span class="n">add</span><span class="p" data-group-id="5483807206-12">(</span><span class="n">beginning_of_day</span><span class="p">,</span><span class="w"> </span><span class="o">-</span><span class="mi">1</span><span class="p">,</span><span class="w"> </span><span class="ss">:day</span><span class="p" data-group-id="5483807206-12">)</span><span class="p" data-group-id="5483807206-11">}</span><span class="p" data-group-id="5483807206-10">)</span><span class="w">
    </span><span class="n">record_fixture</span><span class="p" data-group-id="5483807206-13">(</span><span class="p" data-group-id="5483807206-14">%{</span><span class="ss">inserted_at</span><span class="p">:</span><span class="w"> </span><span class="nc">DateTime</span><span class="o">.</span><span class="n">add</span><span class="p" data-group-id="5483807206-15">(</span><span class="n">beginning_of_day</span><span class="p">,</span><span class="w"> </span><span class="o">-</span><span class="mi">1</span><span class="p">,</span><span class="w"> </span><span class="ss">:hour</span><span class="p" data-group-id="5483807206-15">)</span><span class="p" data-group-id="5483807206-14">}</span><span class="p" data-group-id="5483807206-13">)</span><span class="w">
    </span><span class="n">record_fixture</span><span class="p" data-group-id="5483807206-16">(</span><span class="p" data-group-id="5483807206-17">%{</span><span class="ss">inserted_at</span><span class="p">:</span><span class="w"> </span><span class="nc">DateTime</span><span class="o">.</span><span class="n">add</span><span class="p" data-group-id="5483807206-18">(</span><span class="n">beginning_of_day</span><span class="p">,</span><span class="w"> </span><span class="o">-</span><span class="mi">1</span><span class="p">,</span><span class="w"> </span><span class="ss">:minute</span><span class="p" data-group-id="5483807206-18">)</span><span class="p" data-group-id="5483807206-17">}</span><span class="p" data-group-id="5483807206-16">)</span><span class="w">
    </span><span class="n">record_fixture</span><span class="p" data-group-id="5483807206-19">(</span><span class="p" data-group-id="5483807206-20">%{</span><span class="ss">inserted_at</span><span class="p">:</span><span class="w"> </span><span class="n">beginning_of_day</span><span class="p" data-group-id="5483807206-20">}</span><span class="p" data-group-id="5483807206-19">)</span><span class="w">

    </span><span class="n">assert</span><span class="w"> </span><span class="p" data-group-id="5483807206-21">[</span><span class="n">count_1</span><span class="p">,</span><span class="w"> </span><span class="n">count_2</span><span class="p">,</span><span class="w"> </span><span class="n">count_3</span><span class="p" data-group-id="5483807206-21">]</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">Records</span><span class="o">.</span><span class="n">list_record_counts_over_time</span><span class="p" data-group-id="5483807206-22">(</span><span class="p" data-group-id="5483807206-23">{</span><span class="ss">:day</span><span class="p">,</span><span class="w"> </span><span class="mi">1</span><span class="p" data-group-id="5483807206-23">}</span><span class="p" data-group-id="5483807206-22">)</span><span class="w">
    </span><span class="n">assert</span><span class="w"> </span><span class="n">count_1</span><span class="o">.</span><span class="n">timestamp</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="nc">NaiveDateTime</span><span class="o">.</span><span class="n">beginning_of_day</span><span class="p" data-group-id="5483807206-24">(</span><span class="nc">NaiveDateTime</span><span class="o">.</span><span class="n">add</span><span class="p" data-group-id="5483807206-25">(</span><span class="n">beginning_of_day</span><span class="p">,</span><span class="w"> </span><span class="o">-</span><span class="mi">2</span><span class="p">,</span><span class="w"> </span><span class="ss">:day</span><span class="p" data-group-id="5483807206-25">)</span><span class="p" data-group-id="5483807206-24">)</span><span class="w">
    </span><span class="n">assert</span><span class="w"> </span><span class="n">count_1</span><span class="o">.</span><span class="n">count</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="mi">1</span><span class="w">
    </span><span class="n">assert</span><span class="w"> </span><span class="n">count_2</span><span class="o">.</span><span class="n">timestamp</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="nc">NaiveDateTime</span><span class="o">.</span><span class="n">beginning_of_day</span><span class="p" data-group-id="5483807206-26">(</span><span class="nc">NaiveDateTime</span><span class="o">.</span><span class="n">add</span><span class="p" data-group-id="5483807206-27">(</span><span class="n">beginning_of_day</span><span class="p">,</span><span class="w"> </span><span class="o">-</span><span class="mi">1</span><span class="p">,</span><span class="w"> </span><span class="ss">:day</span><span class="p" data-group-id="5483807206-27">)</span><span class="p" data-group-id="5483807206-26">)</span><span class="w">
    </span><span class="n">assert</span><span class="w"> </span><span class="n">count_2</span><span class="o">.</span><span class="n">count</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="mi">3</span><span class="w">
    </span><span class="n">assert</span><span class="w"> </span><span class="n">count_3</span><span class="o">.</span><span class="n">timestamp</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="nc">NaiveDateTime</span><span class="o">.</span><span class="n">beginning_of_day</span><span class="p" data-group-id="5483807206-28">(</span><span class="n">beginning_of_day</span><span class="p" data-group-id="5483807206-28">)</span><span class="w">
    </span><span class="n">assert</span><span class="w"> </span><span class="n">count_3</span><span class="o">.</span><span class="n">count</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="mi">1</span><span class="w">
  </span><span class="k" data-group-id="5483807206-3">end</span><span class="w">

  </span><span class="kd">defp</span><span class="w"> </span><span class="nf">record_fixture</span><span class="p" data-group-id="5483807206-29">(</span><span class="n">attrs</span><span class="p" data-group-id="5483807206-29">)</span><span class="w"> </span><span class="k" data-group-id="5483807206-30">do</span><span class="w">
    </span><span class="nc">Record</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">struct!</span><span class="p" data-group-id="5483807206-31">(</span><span class="n">attrs</span><span class="p" data-group-id="5483807206-31">)</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="nc">Repo</span><span class="o">.</span><span class="n">insert!</span><span class="p" data-group-id="5483807206-32">(</span><span class="p" data-group-id="5483807206-32">)</span><span class="w">
  </span><span class="k" data-group-id="5483807206-30">end</span><span class="w">
</span><span class="k" data-group-id="5483807206-1">end</span></code></pre>
</details>
<h2>
Summarizations</h2>
<p>
Ecto with PostgreSQL makes it easy to calculate summarizations such as p-values when querying values over time:</p>
<pre><code class="makeup elixir"><span class="kd">defmodule</span><span class="w"> </span><span class="nc">MyApp.Records</span><span class="w"> </span><span class="k" data-group-id="9159473104-1">do</span><span class="w">
  </span><span class="c1"># ...</span><span class="w">

  </span><span class="kd">def</span><span class="w"> </span><span class="nf">list_record_percentiles_over_time</span><span class="p" data-group-id="9159473104-2">(</span><span class="n">time_interval</span><span class="p" data-group-id="9159473104-2">)</span><span class="w"> </span><span class="k" data-group-id="9159473104-3">do</span><span class="w">
    </span><span class="nc">Record</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">select</span><span class="p" data-group-id="9159473104-4">(</span><span class="p" data-group-id="9159473104-5">[</span><span class="n">r</span><span class="p" data-group-id="9159473104-5">]</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="9159473104-6">%{</span><span class="w">
      </span><span class="ss">ended_at</span><span class="p">:</span><span class="w"> </span><span class="n">r</span><span class="o">.</span><span class="n">ended_at</span><span class="p">,</span><span class="w">
      </span><span class="ss">duration</span><span class="p">:</span><span class="w"> </span><span class="n">fragment</span><span class="p" data-group-id="9159473104-7">(</span><span class="s">&quot;extract(epoch from ? - ?)&quot;</span><span class="p">,</span><span class="w"> </span><span class="n">r</span><span class="o">.</span><span class="n">ended_at</span><span class="p">,</span><span class="w"> </span><span class="n">r</span><span class="o">.</span><span class="n">began_at</span><span class="p" data-group-id="9159473104-7">)</span><span class="p">,</span><span class="w">
    </span><span class="p" data-group-id="9159473104-6">}</span><span class="p" data-group-id="9159473104-4">)</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">subquery</span><span class="p" data-group-id="9159473104-8">(</span><span class="p" data-group-id="9159473104-8">)</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">from</span><span class="p" data-group-id="9159473104-9">(</span><span class="p" data-group-id="9159473104-9">)</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">group_by</span><span class="p" data-group-id="9159473104-10">(</span><span class="p" data-group-id="9159473104-11">[</span><span class="n">r</span><span class="p" data-group-id="9159473104-11">]</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="9159473104-12">[</span><span class="n">fragment</span><span class="p" data-group-id="9159473104-13">(</span><span class="s">&quot;timestamp&quot;</span><span class="p" data-group-id="9159473104-13">)</span><span class="p" data-group-id="9159473104-12">]</span><span class="p" data-group-id="9159473104-10">)</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">select</span><span class="p" data-group-id="9159473104-14">(</span><span class="p" data-group-id="9159473104-15">[</span><span class="n">r</span><span class="p" data-group-id="9159473104-15">]</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="9159473104-16">%{</span><span class="w">
      </span><span class="ss">timestamp</span><span class="p">:</span><span class="w"> </span><span class="n">fragment</span><span class="p" data-group-id="9159473104-17">(</span><span class="s">&quot;date_bin(?, ?, &#39;epoch&#39;) as timestamp&quot;</span><span class="p">,</span><span class="w"> </span><span class="o">^</span><span class="n">to_interval</span><span class="p" data-group-id="9159473104-18">(</span><span class="n">time_interval</span><span class="p" data-group-id="9159473104-18">)</span><span class="p">,</span><span class="w"> </span><span class="n">r</span><span class="o">.</span><span class="n">ended_at</span><span class="p" data-group-id="9159473104-17">)</span><span class="p">,</span><span class="w">
      </span><span class="ss">min</span><span class="p">:</span><span class="w"> </span><span class="n">min</span><span class="p" data-group-id="9159473104-19">(</span><span class="n">r</span><span class="o">.</span><span class="n">duration</span><span class="p" data-group-id="9159473104-19">)</span><span class="p">,</span><span class="w">
      </span><span class="ss">p25</span><span class="p">:</span><span class="w"> </span><span class="n">type</span><span class="p" data-group-id="9159473104-20">(</span><span class="n">fragment</span><span class="p" data-group-id="9159473104-21">(</span><span class="s">&quot;percentile_cont (0.25) WITHIN GROUP (ORDER BY ?)&quot;</span><span class="p">,</span><span class="w"> </span><span class="n">r</span><span class="o">.</span><span class="n">duration</span><span class="p" data-group-id="9159473104-21">)</span><span class="p">,</span><span class="w"> </span><span class="ss">:decimal</span><span class="p" data-group-id="9159473104-20">)</span><span class="p">,</span><span class="w">
      </span><span class="ss">p50</span><span class="p">:</span><span class="w"> </span><span class="n">type</span><span class="p" data-group-id="9159473104-22">(</span><span class="n">fragment</span><span class="p" data-group-id="9159473104-23">(</span><span class="s">&quot;percentile_cont (0.5) WITHIN GROUP (ORDER BY ?)&quot;</span><span class="p">,</span><span class="w"> </span><span class="n">r</span><span class="o">.</span><span class="n">duration</span><span class="p" data-group-id="9159473104-23">)</span><span class="p">,</span><span class="w"> </span><span class="ss">:decimal</span><span class="p" data-group-id="9159473104-22">)</span><span class="p">,</span><span class="w">
      </span><span class="ss">p75</span><span class="p">:</span><span class="w"> </span><span class="n">type</span><span class="p" data-group-id="9159473104-24">(</span><span class="n">fragment</span><span class="p" data-group-id="9159473104-25">(</span><span class="s">&quot;percentile_cont (0.75) WITHIN GROUP (ORDER BY ?)&quot;</span><span class="p">,</span><span class="w"> </span><span class="n">r</span><span class="o">.</span><span class="n">duration</span><span class="p" data-group-id="9159473104-25">)</span><span class="p">,</span><span class="w"> </span><span class="ss">:decimal</span><span class="p" data-group-id="9159473104-24">)</span><span class="p">,</span><span class="w">
      </span><span class="ss">max</span><span class="p">:</span><span class="w"> </span><span class="n">max</span><span class="p" data-group-id="9159473104-26">(</span><span class="n">r</span><span class="o">.</span><span class="n">duration</span><span class="p" data-group-id="9159473104-26">)</span><span class="w">
    </span><span class="p" data-group-id="9159473104-16">}</span><span class="p" data-group-id="9159473104-14">)</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">order_by</span><span class="p" data-group-id="9159473104-27">(</span><span class="p" data-group-id="9159473104-28">[</span><span class="n">r</span><span class="p" data-group-id="9159473104-28">]</span><span class="p">,</span><span class="w"> </span><span class="ss">asc</span><span class="p">:</span><span class="w"> </span><span class="n">fragment</span><span class="p" data-group-id="9159473104-29">(</span><span class="s">&quot;timestamp&quot;</span><span class="p" data-group-id="9159473104-29">)</span><span class="p" data-group-id="9159473104-27">)</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="nc">Repo</span><span class="o">.</span><span class="n">all</span><span class="p" data-group-id="9159473104-30">(</span><span class="p" data-group-id="9159473104-30">)</span><span class="w">
  </span><span class="k" data-group-id="9159473104-3">end</span><span class="w">
</span><span class="k" data-group-id="9159473104-1">end</span></code></pre>
<details><summary class="cursor-pointer">
Tests for <code class="inline">time_interval/2</code> and <code class="inline">list_record_counts_over_time/1</code>.
</summary>
<pre><code class="makeup elixir"><span class="kd">defmodule</span><span class="w"> </span><span class="nc">MyApp.RecordsTest</span><span class="w"> </span><span class="k" data-group-id="0270775419-1">do</span><span class="w">
  </span><span class="kn">use</span><span class="w"> </span><span class="nc">MyApp.DataCase</span><span class="w">

  </span><span class="c1"># ...</span><span class="w">

  </span><span class="n">test</span><span class="w"> </span><span class="s">&quot;list_record_percentiles_over_time/1&quot;</span><span class="w"> </span><span class="k" data-group-id="0270775419-2">do</span><span class="w">
    </span><span class="n">beginning_of_day</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p" data-group-id="0270775419-3">%{</span><span class="nc">DateTime</span><span class="o">.</span><span class="n">utc_now</span><span class="p" data-group-id="0270775419-4">(</span><span class="p" data-group-id="0270775419-4">)</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="ss">hour</span><span class="p">:</span><span class="w"> </span><span class="mi">0</span><span class="p">,</span><span class="w"> </span><span class="ss">minute</span><span class="p">:</span><span class="w"> </span><span class="mi">0</span><span class="p">,</span><span class="w"> </span><span class="ss">second</span><span class="p">:</span><span class="w"> </span><span class="mi">0</span><span class="p">,</span><span class="w"> </span><span class="ss">microsecond</span><span class="p">:</span><span class="w"> </span><span class="p" data-group-id="0270775419-5">{</span><span class="mi">0</span><span class="p">,</span><span class="w"> </span><span class="mi">6</span><span class="p" data-group-id="0270775419-5">}</span><span class="p" data-group-id="0270775419-3">}</span><span class="w">

    </span><span class="n">record_fixture</span><span class="p" data-group-id="0270775419-6">(</span><span class="p" data-group-id="0270775419-7">%{</span><span class="ss">began_at</span><span class="p">:</span><span class="w"> </span><span class="nc">DateTime</span><span class="o">.</span><span class="n">add</span><span class="p" data-group-id="0270775419-8">(</span><span class="n">beginning_of_day</span><span class="p">,</span><span class="w"> </span><span class="o">-</span><span class="mi">2</span><span class="p">,</span><span class="w"> </span><span class="ss">:day</span><span class="p" data-group-id="0270775419-8">)</span><span class="p">,</span><span class="w"> </span><span class="ss">ended_at</span><span class="p">:</span><span class="w"> </span><span class="nc">DateTime</span><span class="o">.</span><span class="n">add</span><span class="p" data-group-id="0270775419-9">(</span><span class="n">beginning_of_day</span><span class="p">,</span><span class="w"> </span><span class="o">-</span><span class="mi">1</span><span class="p">,</span><span class="w"> </span><span class="ss">:day</span><span class="p" data-group-id="0270775419-9">)</span><span class="p" data-group-id="0270775419-7">}</span><span class="p" data-group-id="0270775419-6">)</span><span class="w">
    </span><span class="n">record_fixture</span><span class="p" data-group-id="0270775419-10">(</span><span class="p" data-group-id="0270775419-11">%{</span><span class="ss">began_at</span><span class="p">:</span><span class="w"> </span><span class="nc">DateTime</span><span class="o">.</span><span class="n">add</span><span class="p" data-group-id="0270775419-12">(</span><span class="n">beginning_of_day</span><span class="p">,</span><span class="w"> </span><span class="o">-</span><span class="mi">2</span><span class="p">,</span><span class="w"> </span><span class="ss">:day</span><span class="p" data-group-id="0270775419-12">)</span><span class="p">,</span><span class="w"> </span><span class="ss">ended_at</span><span class="p">:</span><span class="w"> </span><span class="n">beginning_of_day</span><span class="p" data-group-id="0270775419-11">}</span><span class="p" data-group-id="0270775419-10">)</span><span class="w">
    </span><span class="n">record_fixture</span><span class="p" data-group-id="0270775419-13">(</span><span class="p" data-group-id="0270775419-14">%{</span><span class="ss">began_at</span><span class="p">:</span><span class="w"> </span><span class="nc">DateTime</span><span class="o">.</span><span class="n">add</span><span class="p" data-group-id="0270775419-15">(</span><span class="n">beginning_of_day</span><span class="p">,</span><span class="w"> </span><span class="o">-</span><span class="mi">1</span><span class="p">,</span><span class="w"> </span><span class="ss">:day</span><span class="p" data-group-id="0270775419-15">)</span><span class="p">,</span><span class="w"> </span><span class="ss">ended_at</span><span class="p">:</span><span class="w"> </span><span class="nc">DateTime</span><span class="o">.</span><span class="n">add</span><span class="p" data-group-id="0270775419-16">(</span><span class="n">beginning_of_day</span><span class="p">,</span><span class="w"> </span><span class="mi">12</span><span class="p">,</span><span class="w"> </span><span class="ss">:hour</span><span class="p" data-group-id="0270775419-16">)</span><span class="p" data-group-id="0270775419-14">}</span><span class="p" data-group-id="0270775419-13">)</span><span class="w">

    </span><span class="n">assert</span><span class="w"> </span><span class="p" data-group-id="0270775419-17">[</span><span class="n">precentile_1</span><span class="p">,</span><span class="w"> </span><span class="n">precentile_2</span><span class="p" data-group-id="0270775419-17">]</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">Records</span><span class="o">.</span><span class="n">list_record_percentiles_over_time</span><span class="p" data-group-id="0270775419-18">(</span><span class="p" data-group-id="0270775419-19">{</span><span class="ss">:day</span><span class="p">,</span><span class="w"> </span><span class="mi">1</span><span class="p" data-group-id="0270775419-19">}</span><span class="p" data-group-id="0270775419-18">)</span><span class="w">
    </span><span class="n">assert</span><span class="w"> </span><span class="n">precentile_1</span><span class="o">.</span><span class="n">timestamp</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="nc">NaiveDateTime</span><span class="o">.</span><span class="n">add</span><span class="p" data-group-id="0270775419-20">(</span><span class="n">beginning_of_day</span><span class="p">,</span><span class="w"> </span><span class="o">-</span><span class="mi">1</span><span class="p">,</span><span class="w"> </span><span class="ss">:day</span><span class="p" data-group-id="0270775419-20">)</span><span class="w">
    </span><span class="n">assert</span><span class="w"> </span><span class="nc">Decimal</span><span class="o">.</span><span class="n">eq?</span><span class="p" data-group-id="0270775419-21">(</span><span class="n">precentile_1</span><span class="o">.</span><span class="n">min</span><span class="p">,</span><span class="w"> </span><span class="mi">60</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="mi">60</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="mi">24</span><span class="p" data-group-id="0270775419-21">)</span><span class="w">
    </span><span class="n">assert</span><span class="w"> </span><span class="nc">Decimal</span><span class="o">.</span><span class="n">eq?</span><span class="p" data-group-id="0270775419-22">(</span><span class="n">precentile_1</span><span class="o">.</span><span class="n">max</span><span class="p">,</span><span class="w"> </span><span class="mi">60</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="mi">60</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="mi">24</span><span class="p" data-group-id="0270775419-22">)</span><span class="w">
    </span><span class="n">assert</span><span class="w"> </span><span class="n">precentile_2</span><span class="o">.</span><span class="n">timestamp</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="nc">DateTime</span><span class="o">.</span><span class="n">to_naive</span><span class="p" data-group-id="0270775419-23">(</span><span class="n">beginning_of_day</span><span class="p" data-group-id="0270775419-23">)</span><span class="w">
    </span><span class="n">assert</span><span class="w"> </span><span class="nc">Decimal</span><span class="o">.</span><span class="n">eq?</span><span class="p" data-group-id="0270775419-24">(</span><span class="n">precentile_2</span><span class="o">.</span><span class="n">min</span><span class="p">,</span><span class="w"> </span><span class="mi">60</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="mi">60</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="mi">36</span><span class="p" data-group-id="0270775419-24">)</span><span class="w">
    </span><span class="n">assert</span><span class="w"> </span><span class="nc">Decimal</span><span class="o">.</span><span class="n">eq?</span><span class="p" data-group-id="0270775419-25">(</span><span class="n">precentile_2</span><span class="o">.</span><span class="n">max</span><span class="p">,</span><span class="w"> </span><span class="mi">60</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="mi">60</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="mi">48</span><span class="p" data-group-id="0270775419-25">)</span><span class="w">
    </span><span class="n">assert</span><span class="w"> </span><span class="nc">Decimal</span><span class="o">.</span><span class="n">eq?</span><span class="p" data-group-id="0270775419-26">(</span><span class="n">precentile_2</span><span class="o">.</span><span class="n">p25</span><span class="p">,</span><span class="w"> </span><span class="mi">60</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="mi">60</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="mi">39</span><span class="p" data-group-id="0270775419-26">)</span><span class="w">
    </span><span class="n">assert</span><span class="w"> </span><span class="nc">Decimal</span><span class="o">.</span><span class="n">eq?</span><span class="p" data-group-id="0270775419-27">(</span><span class="n">precentile_2</span><span class="o">.</span><span class="n">p50</span><span class="p">,</span><span class="w"> </span><span class="mi">60</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="mi">60</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="mi">42</span><span class="p" data-group-id="0270775419-27">)</span><span class="w">
    </span><span class="n">assert</span><span class="w"> </span><span class="nc">Decimal</span><span class="o">.</span><span class="n">eq?</span><span class="p" data-group-id="0270775419-28">(</span><span class="n">precentile_2</span><span class="o">.</span><span class="n">p75</span><span class="p">,</span><span class="w"> </span><span class="mi">60</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="mi">60</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="mi">45</span><span class="p" data-group-id="0270775419-28">)</span><span class="w">
  </span><span class="k" data-group-id="0270775419-2">end</span><span class="w">
</span><span class="k" data-group-id="0270775419-1">end</span></code></pre>
</details>
<h2>
Filling the gaps</h2>
<p>
It is common to experience gaps in your data with the above queries. This is not a problem for all charts, but for continuous charts like line charts, you’ll need to fill them in.</p>
<p>
You have two options:</p>
<h3>
1. Use series generation in the PostgreSQL query</h3>
<p>
You’ll need to fetch the minimum and maximum values before you can generate the series:</p>
<pre><code class="makeup elixir"><span class="kd">defmodule</span><span class="w"> </span><span class="nc">MyApp.Records</span><span class="w"> </span><span class="k" data-group-id="9533334097-1">do</span><span class="w">
  </span><span class="c1"># ...</span><span class="w">

  </span><span class="kd">def</span><span class="w"> </span><span class="nf">list_record_counts_over_time</span><span class="p" data-group-id="9533334097-2">(</span><span class="n">time_interval</span><span class="p" data-group-id="9533334097-2">)</span><span class="w"> </span><span class="k" data-group-id="9533334097-3">do</span><span class="w">
    </span><span class="n">interval</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">to_interval</span><span class="p" data-group-id="9533334097-4">(</span><span class="n">time_interval</span><span class="p" data-group-id="9533334097-4">)</span><span class="w">

    </span><span class="nc">Record</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">select</span><span class="p" data-group-id="9533334097-5">(</span><span class="p" data-group-id="9533334097-6">[</span><span class="n">r</span><span class="p" data-group-id="9533334097-6">]</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="9533334097-7">%{</span><span class="ss">min</span><span class="p">:</span><span class="w"> </span><span class="n">min</span><span class="p" data-group-id="9533334097-8">(</span><span class="n">r</span><span class="o">.</span><span class="n">inserted_at</span><span class="p" data-group-id="9533334097-8">)</span><span class="p">,</span><span class="w"> </span><span class="ss">max</span><span class="p">:</span><span class="w"> </span><span class="n">max</span><span class="p" data-group-id="9533334097-9">(</span><span class="n">r</span><span class="o">.</span><span class="n">inserted_at</span><span class="p" data-group-id="9533334097-9">)</span><span class="p" data-group-id="9533334097-7">}</span><span class="p" data-group-id="9533334097-5">)</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="nc">Repo</span><span class="o">.</span><span class="n">one</span><span class="p" data-group-id="9533334097-10">(</span><span class="p" data-group-id="9533334097-10">)</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="k">case</span><span class="w"> </span><span class="k" data-group-id="9533334097-11">do</span><span class="w">
      </span><span class="no">nil</span><span class="w"> </span><span class="o">-&gt;</span><span class="w">
        </span><span class="p" data-group-id="9533334097-12">[</span><span class="p" data-group-id="9533334097-12">]</span><span class="w">

      </span><span class="p" data-group-id="9533334097-13">%{</span><span class="ss">min</span><span class="p">:</span><span class="w"> </span><span class="n">min</span><span class="p">,</span><span class="w"> </span><span class="ss">max</span><span class="p">:</span><span class="w"> </span><span class="n">max</span><span class="p" data-group-id="9533334097-13">}</span><span class="w"> </span><span class="o">-&gt;</span><span class="w">
        </span><span class="n">query</span><span class="w"> </span><span class="o">=</span><span class="w">
          </span><span class="nc">Record</span><span class="w">
          </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">group_by</span><span class="p" data-group-id="9533334097-14">(</span><span class="p" data-group-id="9533334097-15">[</span><span class="n">r</span><span class="p" data-group-id="9533334097-15">]</span><span class="p">,</span><span class="w"> </span><span class="n">fragment</span><span class="p" data-group-id="9533334097-16">(</span><span class="s">&quot;timestamp&quot;</span><span class="p" data-group-id="9533334097-16">)</span><span class="p" data-group-id="9533334097-14">)</span><span class="w">
          </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">select</span><span class="p" data-group-id="9533334097-17">(</span><span class="p" data-group-id="9533334097-18">[</span><span class="n">r</span><span class="p" data-group-id="9533334097-18">]</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="9533334097-19">%{</span><span class="w">
            </span><span class="ss">timestamp</span><span class="p">:</span><span class="w"> </span><span class="n">fragment</span><span class="p" data-group-id="9533334097-20">(</span><span class="s">&quot;date_bin(?, ?, &#39;epoch&#39;)&quot;</span><span class="p">,</span><span class="w"> </span><span class="o">^</span><span class="n">interval</span><span class="p">,</span><span class="w"> </span><span class="n">r</span><span class="o">.</span><span class="n">inserted_at</span><span class="p" data-group-id="9533334097-20">)</span><span class="p">,</span><span class="w">
            </span><span class="ss">count</span><span class="p">:</span><span class="w"> </span><span class="n">count</span><span class="p" data-group-id="9533334097-21">(</span><span class="n">r</span><span class="o">.</span><span class="n">id</span><span class="p" data-group-id="9533334097-21">)</span><span class="w">
          </span><span class="p" data-group-id="9533334097-19">}</span><span class="p" data-group-id="9533334097-17">)</span><span class="w">

        </span><span class="n">from</span><span class="p" data-group-id="9533334097-22">(</span><span class="w">
          </span><span class="n">fragment</span><span class="p" data-group-id="9533334097-23">(</span><span class="s">&quot;GENERATE_SERIES(?::timestamp, ?::timestamp, ?::interval)&quot;</span><span class="p">,</span><span class="w"> </span><span class="o">^</span><span class="n">min</span><span class="p">,</span><span class="w"> </span><span class="o">^</span><span class="n">max</span><span class="p">,</span><span class="w"> </span><span class="o">^</span><span class="n">interval</span><span class="p" data-group-id="9533334097-23">)</span><span class="w">
        </span><span class="p" data-group-id="9533334097-22">)</span><span class="w">
        </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">select</span><span class="p" data-group-id="9533334097-24">(</span><span class="p" data-group-id="9533334097-25">[</span><span class="n">s</span><span class="p" data-group-id="9533334097-25">]</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="9533334097-26">%{</span><span class="ss">timestamp</span><span class="p">:</span><span class="w"> </span><span class="n">fragment</span><span class="p" data-group-id="9533334097-27">(</span><span class="s">&quot;DATE_BIN(?, ?::timestamp, &#39;epoch&#39;)&quot;</span><span class="p">,</span><span class="w"> </span><span class="o">^</span><span class="n">interval</span><span class="p">,</span><span class="w"> </span><span class="n">s</span><span class="p" data-group-id="9533334097-27">)</span><span class="p" data-group-id="9533334097-26">}</span><span class="p" data-group-id="9533334097-24">)</span><span class="w">
        </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">subquery</span><span class="p" data-group-id="9533334097-28">(</span><span class="p" data-group-id="9533334097-28">)</span><span class="w">
        </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">join</span><span class="p" data-group-id="9533334097-29">(</span><span class="ss">:left</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="9533334097-30">[</span><span class="n">s</span><span class="p" data-group-id="9533334097-30">]</span><span class="p">,</span><span class="w"> </span><span class="n">r</span><span class="w"> </span><span class="ow">in</span><span class="w"> </span><span class="n">subquery</span><span class="p" data-group-id="9533334097-31">(</span><span class="n">query</span><span class="p" data-group-id="9533334097-31">)</span><span class="p">,</span><span class="w"> </span><span class="ss">on</span><span class="p">:</span><span class="w"> </span><span class="n">s</span><span class="o">.</span><span class="n">timestamp</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="n">r</span><span class="o">.</span><span class="n">timestamp</span><span class="p" data-group-id="9533334097-29">)</span><span class="w">
        </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">order_by</span><span class="p" data-group-id="9533334097-32">(</span><span class="p" data-group-id="9533334097-33">[</span><span class="n">s</span><span class="p" data-group-id="9533334097-33">]</span><span class="p">,</span><span class="w"> </span><span class="ss">asc</span><span class="p">:</span><span class="w"> </span><span class="n">s</span><span class="o">.</span><span class="n">timestamp</span><span class="p" data-group-id="9533334097-32">)</span><span class="w">
        </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">select</span><span class="p" data-group-id="9533334097-34">(</span><span class="p" data-group-id="9533334097-35">[</span><span class="n">s</span><span class="p">,</span><span class="w"> </span><span class="n">r</span><span class="p" data-group-id="9533334097-35">]</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="9533334097-36">%{</span><span class="w">
          </span><span class="ss">timestamp</span><span class="p">:</span><span class="w"> </span><span class="n">s</span><span class="o">.</span><span class="n">timestamp</span><span class="p">,</span><span class="w">
          </span><span class="ss">count</span><span class="p">:</span><span class="w"> </span><span class="n">coalesce</span><span class="p" data-group-id="9533334097-37">(</span><span class="n">r</span><span class="o">.</span><span class="n">count</span><span class="p">,</span><span class="w"> </span><span class="mi">0</span><span class="p" data-group-id="9533334097-37">)</span><span class="w">
        </span><span class="p" data-group-id="9533334097-36">}</span><span class="p" data-group-id="9533334097-34">)</span><span class="w">
        </span><span class="o">|&gt;</span><span class="w"> </span><span class="nc">Repo</span><span class="o">.</span><span class="n">all</span><span class="p" data-group-id="9533334097-38">(</span><span class="p" data-group-id="9533334097-38">)</span><span class="w">
    </span><span class="k" data-group-id="9533334097-11">end</span><span class="w">
  </span><span class="k" data-group-id="9533334097-3">end</span><span class="w">
</span><span class="k" data-group-id="9533334097-1">end</span></code></pre>
<details><summary class="cursor-pointer">
Tests for <code class="inline">list_record_counts_over_time/1</code>.
</summary>
<pre><code class="makeup elixir"><span class="kd">defmodule</span><span class="w"> </span><span class="nc">MyApp.RecordsTest</span><span class="w"> </span><span class="k" data-group-id="8355656989-1">do</span><span class="w">
  </span><span class="kn">use</span><span class="w"> </span><span class="nc">MyApp.DataCase</span><span class="w">

  </span><span class="c1"># ...</span><span class="w">

  </span><span class="n">test</span><span class="w"> </span><span class="s">&quot;list_record_counts_over_time/1&quot;</span><span class="w"> </span><span class="k" data-group-id="8355656989-2">do</span><span class="w">
    </span><span class="n">assert</span><span class="w"> </span><span class="nc">Records</span><span class="o">.</span><span class="n">list_record_counts_over_time</span><span class="p" data-group-id="8355656989-3">(</span><span class="p" data-group-id="8355656989-4">{</span><span class="ss">:day</span><span class="p">,</span><span class="w"> </span><span class="mi">1</span><span class="p" data-group-id="8355656989-4">}</span><span class="p" data-group-id="8355656989-3">)</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="p" data-group-id="8355656989-5">[</span><span class="p" data-group-id="8355656989-5">]</span><span class="w">

    </span><span class="n">beginning_of_day</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p" data-group-id="8355656989-6">%{</span><span class="nc">DateTime</span><span class="o">.</span><span class="n">utc_now</span><span class="p" data-group-id="8355656989-7">(</span><span class="p" data-group-id="8355656989-7">)</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="ss">hour</span><span class="p">:</span><span class="w"> </span><span class="mi">0</span><span class="p">,</span><span class="w"> </span><span class="ss">minute</span><span class="p">:</span><span class="w"> </span><span class="mi">0</span><span class="p">,</span><span class="w"> </span><span class="ss">second</span><span class="p">:</span><span class="w"> </span><span class="mi">0</span><span class="p">,</span><span class="w"> </span><span class="ss">microsecond</span><span class="p">:</span><span class="w"> </span><span class="p" data-group-id="8355656989-8">{</span><span class="mi">0</span><span class="p">,</span><span class="w"> </span><span class="mi">6</span><span class="p" data-group-id="8355656989-8">}</span><span class="p" data-group-id="8355656989-6">}</span><span class="w">

    </span><span class="n">record_fixture</span><span class="p" data-group-id="8355656989-9">(</span><span class="p" data-group-id="8355656989-10">%{</span><span class="ss">inserted_at</span><span class="p">:</span><span class="w"> </span><span class="nc">DateTime</span><span class="o">.</span><span class="n">add</span><span class="p" data-group-id="8355656989-11">(</span><span class="n">beginning_of_day</span><span class="p">,</span><span class="w"> </span><span class="o">-</span><span class="mi">3</span><span class="p">,</span><span class="w"> </span><span class="ss">:day</span><span class="p" data-group-id="8355656989-11">)</span><span class="p" data-group-id="8355656989-10">}</span><span class="p" data-group-id="8355656989-9">)</span><span class="w">
    </span><span class="n">record_fixture</span><span class="p" data-group-id="8355656989-12">(</span><span class="p" data-group-id="8355656989-13">%{</span><span class="ss">inserted_at</span><span class="p">:</span><span class="w"> </span><span class="nc">DateTime</span><span class="o">.</span><span class="n">add</span><span class="p" data-group-id="8355656989-14">(</span><span class="n">beginning_of_day</span><span class="p">,</span><span class="w"> </span><span class="o">-</span><span class="mi">1</span><span class="p">,</span><span class="w"> </span><span class="ss">:day</span><span class="p" data-group-id="8355656989-14">)</span><span class="p" data-group-id="8355656989-13">}</span><span class="p" data-group-id="8355656989-12">)</span><span class="w">
    </span><span class="n">record_fixture</span><span class="p" data-group-id="8355656989-15">(</span><span class="p" data-group-id="8355656989-16">%{</span><span class="ss">inserted_at</span><span class="p">:</span><span class="w"> </span><span class="nc">DateTime</span><span class="o">.</span><span class="n">add</span><span class="p" data-group-id="8355656989-17">(</span><span class="n">beginning_of_day</span><span class="p">,</span><span class="w"> </span><span class="o">-</span><span class="mi">1</span><span class="p">,</span><span class="w"> </span><span class="ss">:hour</span><span class="p" data-group-id="8355656989-17">)</span><span class="p" data-group-id="8355656989-16">}</span><span class="p" data-group-id="8355656989-15">)</span><span class="w">
    </span><span class="n">record_fixture</span><span class="p" data-group-id="8355656989-18">(</span><span class="p" data-group-id="8355656989-19">%{</span><span class="ss">inserted_at</span><span class="p">:</span><span class="w"> </span><span class="nc">DateTime</span><span class="o">.</span><span class="n">add</span><span class="p" data-group-id="8355656989-20">(</span><span class="n">beginning_of_day</span><span class="p">,</span><span class="w"> </span><span class="o">-</span><span class="mi">1</span><span class="p">,</span><span class="w"> </span><span class="ss">:minute</span><span class="p" data-group-id="8355656989-20">)</span><span class="p" data-group-id="8355656989-19">}</span><span class="p" data-group-id="8355656989-18">)</span><span class="w">
    </span><span class="n">record_fixture</span><span class="p" data-group-id="8355656989-21">(</span><span class="p" data-group-id="8355656989-22">%{</span><span class="ss">inserted_at</span><span class="p">:</span><span class="w"> </span><span class="n">beginning_of_day</span><span class="p" data-group-id="8355656989-22">}</span><span class="p" data-group-id="8355656989-21">)</span><span class="w">

    </span><span class="n">assert</span><span class="w"> </span><span class="p" data-group-id="8355656989-23">[</span><span class="n">count_1</span><span class="p">,</span><span class="w"> </span><span class="n">count_2</span><span class="p">,</span><span class="w"> </span><span class="n">count_3</span><span class="p">,</span><span class="w"> </span><span class="n">count_4</span><span class="p" data-group-id="8355656989-23">]</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">Records</span><span class="o">.</span><span class="n">list_record_counts_over_time_2</span><span class="p" data-group-id="8355656989-24">(</span><span class="p" data-group-id="8355656989-25">{</span><span class="ss">:day</span><span class="p">,</span><span class="w"> </span><span class="mi">1</span><span class="p" data-group-id="8355656989-25">}</span><span class="p" data-group-id="8355656989-24">)</span><span class="w">
    </span><span class="n">assert</span><span class="w"> </span><span class="n">count_1</span><span class="o">.</span><span class="n">timestamp</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="nc">NaiveDateTime</span><span class="o">.</span><span class="n">beginning_of_day</span><span class="p" data-group-id="8355656989-26">(</span><span class="nc">NaiveDateTime</span><span class="o">.</span><span class="n">add</span><span class="p" data-group-id="8355656989-27">(</span><span class="n">beginning_of_day</span><span class="p">,</span><span class="w"> </span><span class="o">-</span><span class="mi">3</span><span class="p">,</span><span class="w"> </span><span class="ss">:day</span><span class="p" data-group-id="8355656989-27">)</span><span class="p" data-group-id="8355656989-26">)</span><span class="w">
    </span><span class="n">assert</span><span class="w"> </span><span class="n">count_1</span><span class="o">.</span><span class="n">count</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="mi">1</span><span class="w">
    </span><span class="n">assert</span><span class="w"> </span><span class="n">count_2</span><span class="o">.</span><span class="n">timestamp</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="nc">NaiveDateTime</span><span class="o">.</span><span class="n">beginning_of_day</span><span class="p" data-group-id="8355656989-28">(</span><span class="nc">NaiveDateTime</span><span class="o">.</span><span class="n">add</span><span class="p" data-group-id="8355656989-29">(</span><span class="n">beginning_of_day</span><span class="p">,</span><span class="w"> </span><span class="o">-</span><span class="mi">2</span><span class="p">,</span><span class="w"> </span><span class="ss">:day</span><span class="p" data-group-id="8355656989-29">)</span><span class="p" data-group-id="8355656989-28">)</span><span class="w">
    </span><span class="n">assert</span><span class="w"> </span><span class="n">count_2</span><span class="o">.</span><span class="n">count</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="mi">0</span><span class="w">
    </span><span class="n">assert</span><span class="w"> </span><span class="n">count_3</span><span class="o">.</span><span class="n">timestamp</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="nc">NaiveDateTime</span><span class="o">.</span><span class="n">beginning_of_day</span><span class="p" data-group-id="8355656989-30">(</span><span class="nc">NaiveDateTime</span><span class="o">.</span><span class="n">add</span><span class="p" data-group-id="8355656989-31">(</span><span class="n">beginning_of_day</span><span class="p">,</span><span class="w"> </span><span class="o">-</span><span class="mi">1</span><span class="p">,</span><span class="w"> </span><span class="ss">:day</span><span class="p" data-group-id="8355656989-31">)</span><span class="p" data-group-id="8355656989-30">)</span><span class="w">
    </span><span class="n">assert</span><span class="w"> </span><span class="n">count_3</span><span class="o">.</span><span class="n">count</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="mi">3</span><span class="w">
    </span><span class="n">assert</span><span class="w"> </span><span class="n">count_4</span><span class="o">.</span><span class="n">timestamp</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="nc">NaiveDateTime</span><span class="o">.</span><span class="n">beginning_of_day</span><span class="p" data-group-id="8355656989-32">(</span><span class="n">beginning_of_day</span><span class="p" data-group-id="8355656989-32">)</span><span class="w">
    </span><span class="n">assert</span><span class="w"> </span><span class="n">count_4</span><span class="o">.</span><span class="n">count</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="mi">1</span><span class="w">
  </span><span class="k" data-group-id="8355656989-2">end</span><span class="w">
</span><span class="k" data-group-id="8355656989-1">end</span></code></pre>
</details>
<p>
It’ll be the similar process for numeric steps, but you must ensure the upper and lower boundaries as otherwise the steps may start at odd number (e.g. <code class="inline">435.43</code>, <code class="inline">535.43</code>, <code class="inline">635.43</code>). I’m also guarding against extreme tail end by collecting p99 as a max, and including anything above the p99 in the last step.</p>
<pre><code class="makeup elixir"><span class="kd">defmodule</span><span class="w"> </span><span class="nc">MyApp.Records</span><span class="w"> </span><span class="k" data-group-id="7809665581-1">do</span><span class="w">
  </span><span class="kn">alias</span><span class="w"> </span><span class="nc">MyApp.ChartUtils</span><span class="w">

  </span><span class="c1"># ...</span><span class="w">

  </span><span class="kd">def</span><span class="w"> </span><span class="nf">list_record_value_distribution</span><span class="p" data-group-id="7809665581-2">(</span><span class="n">steps</span><span class="w"> </span><span class="o">\\</span><span class="w"> </span><span class="mi">100</span><span class="p" data-group-id="7809665581-2">)</span><span class="w"> </span><span class="k" data-group-id="7809665581-3">do</span><span class="w">
    </span><span class="nc">Record</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">select</span><span class="p" data-group-id="7809665581-4">(</span><span class="p" data-group-id="7809665581-5">[</span><span class="n">r</span><span class="p" data-group-id="7809665581-5">]</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="7809665581-6">%{</span><span class="w">
      </span><span class="ss">min</span><span class="p">:</span><span class="w"> </span><span class="n">min</span><span class="p" data-group-id="7809665581-7">(</span><span class="n">r</span><span class="o">.</span><span class="n">value</span><span class="p" data-group-id="7809665581-7">)</span><span class="p">,</span><span class="w">
      </span><span class="ss">max</span><span class="p">:</span><span class="w"> </span><span class="n">max</span><span class="p" data-group-id="7809665581-8">(</span><span class="n">r</span><span class="o">.</span><span class="n">value</span><span class="p" data-group-id="7809665581-8">)</span><span class="p">,</span><span class="w">
      </span><span class="ss">p99</span><span class="p">:</span><span class="w"> </span><span class="n">type</span><span class="p" data-group-id="7809665581-9">(</span><span class="n">fragment</span><span class="p" data-group-id="7809665581-10">(</span><span class="s">&quot;percentile_cont (0.99) WITHIN GROUP (ORDER BY ?)&quot;</span><span class="p">,</span><span class="w"> </span><span class="n">r</span><span class="o">.</span><span class="n">value</span><span class="p" data-group-id="7809665581-10">)</span><span class="p">,</span><span class="w"> </span><span class="ss">:decimal</span><span class="p" data-group-id="7809665581-9">)</span><span class="w">
      </span><span class="p" data-group-id="7809665581-6">}</span><span class="p" data-group-id="7809665581-4">)</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="nc">Repo</span><span class="o">.</span><span class="n">one</span><span class="p" data-group-id="7809665581-11">(</span><span class="p" data-group-id="7809665581-11">)</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="k">case</span><span class="w"> </span><span class="k" data-group-id="7809665581-12">do</span><span class="w">
      </span><span class="p" data-group-id="7809665581-13">%{</span><span class="ss">min</span><span class="p">:</span><span class="w"> </span><span class="no">nil</span><span class="p">,</span><span class="w"> </span><span class="ss">max</span><span class="p">:</span><span class="w"> </span><span class="no">nil</span><span class="p" data-group-id="7809665581-13">}</span><span class="w"> </span><span class="o">-&gt;</span><span class="w">
        </span><span class="p" data-group-id="7809665581-14">[</span><span class="p" data-group-id="7809665581-14">]</span><span class="w">

      </span><span class="p" data-group-id="7809665581-15">%{</span><span class="ss">min</span><span class="p">:</span><span class="w"> </span><span class="n">min</span><span class="p">,</span><span class="w"> </span><span class="ss">max</span><span class="p">:</span><span class="w"> </span><span class="n">max</span><span class="p">,</span><span class="w"> </span><span class="ss">p99</span><span class="p">:</span><span class="w"> </span><span class="n">p99</span><span class="p" data-group-id="7809665581-15">}</span><span class="w"> </span><span class="o">-&gt;</span><span class="w">
        </span><span class="n">step_size</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">ChartUtils</span><span class="o">.</span><span class="n">step_size</span><span class="p" data-group-id="7809665581-16">(</span><span class="n">min</span><span class="p">,</span><span class="w"> </span><span class="n">p99</span><span class="p">,</span><span class="w"> </span><span class="n">steps</span><span class="p" data-group-id="7809665581-16">)</span><span class="w">
        </span><span class="n">floor_min</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">lower_bound</span><span class="p" data-group-id="7809665581-17">(</span><span class="n">min</span><span class="p">,</span><span class="w"> </span><span class="n">step_size</span><span class="p" data-group-id="7809665581-17">)</span><span class="w">
        </span><span class="n">ceil_max</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">upper_bound</span><span class="p" data-group-id="7809665581-18">(</span><span class="n">max</span><span class="p">,</span><span class="w"> </span><span class="n">step_size</span><span class="p" data-group-id="7809665581-18">)</span><span class="w">
        </span><span class="n">floor_p99</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">lower_bound</span><span class="p" data-group-id="7809665581-19">(</span><span class="n">p99</span><span class="p">,</span><span class="w"> </span><span class="n">step_size</span><span class="p" data-group-id="7809665581-19">)</span><span class="w">

        </span><span class="n">from</span><span class="p" data-group-id="7809665581-20">(</span><span class="w">
          </span><span class="n">fragment</span><span class="p" data-group-id="7809665581-21">(</span><span class="s">&quot;GENERATE_SERIES(?::numeric, ?::numeric, ?::numeric)&quot;</span><span class="p">,</span><span class="w"> </span><span class="o">^</span><span class="n">floor_min</span><span class="p">,</span><span class="w"> </span><span class="o">^</span><span class="n">floor_p99</span><span class="p">,</span><span class="w"> </span><span class="o">^</span><span class="n">step_size</span><span class="p" data-group-id="7809665581-21">)</span><span class="w">
        </span><span class="p" data-group-id="7809665581-20">)</span><span class="w">
        </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">select</span><span class="p" data-group-id="7809665581-22">(</span><span class="p" data-group-id="7809665581-23">[</span><span class="n">s</span><span class="p" data-group-id="7809665581-23">]</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="7809665581-24">%{</span><span class="w">
            </span><span class="ss">min</span><span class="p">:</span><span class="w"> </span><span class="n">fragment</span><span class="p" data-group-id="7809665581-25">(</span><span class="s">&quot;?&quot;</span><span class="p">,</span><span class="w"> </span><span class="n">s</span><span class="p" data-group-id="7809665581-25">)</span><span class="p">,</span><span class="w">
            </span><span class="c1"># We want to ensure that the last step includes everything beyond p99</span><span class="w">
            </span><span class="ss">max</span><span class="p">:</span><span class="w"> </span><span class="n">fragment</span><span class="p" data-group-id="7809665581-26">(</span><span class="s">&quot;CASE WHEN ? = ? THEN ? ELSE ? + ? END&quot;</span><span class="p">,</span><span class="w"> </span><span class="n">s</span><span class="p">,</span><span class="w"> </span><span class="o">^</span><span class="n">floor_p99</span><span class="p">,</span><span class="w"> </span><span class="o">^</span><span class="n">ceil_max</span><span class="p">,</span><span class="w"> </span><span class="n">s</span><span class="p">,</span><span class="w"> </span><span class="o">^</span><span class="n">step_size</span><span class="p" data-group-id="7809665581-26">)</span><span class="w">
          </span><span class="p" data-group-id="7809665581-24">}</span><span class="p" data-group-id="7809665581-22">)</span><span class="w">
        </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">subquery</span><span class="p" data-group-id="7809665581-27">(</span><span class="p" data-group-id="7809665581-27">)</span><span class="w">
        </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">join</span><span class="p" data-group-id="7809665581-28">(</span><span class="ss">:left</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="7809665581-29">[</span><span class="n">s</span><span class="p" data-group-id="7809665581-29">]</span><span class="p">,</span><span class="w"> </span><span class="n">r</span><span class="w"> </span><span class="ow">in</span><span class="w"> </span><span class="nc">Record</span><span class="p">,</span><span class="w"> </span><span class="ss">on</span><span class="p">:</span><span class="w"> </span><span class="n">r</span><span class="o">.</span><span class="n">value</span><span class="w"> </span><span class="o">&gt;=</span><span class="w"> </span><span class="n">s</span><span class="o">.</span><span class="n">min</span><span class="w"> </span><span class="ow">and</span><span class="w"> </span><span class="n">r</span><span class="o">.</span><span class="n">value</span><span class="w"> </span><span class="o">&lt;</span><span class="w"> </span><span class="n">s</span><span class="o">.</span><span class="n">max</span><span class="p" data-group-id="7809665581-28">)</span><span class="w">
        </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">group_by</span><span class="p" data-group-id="7809665581-30">(</span><span class="p" data-group-id="7809665581-31">[</span><span class="n">s</span><span class="p" data-group-id="7809665581-31">]</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="7809665581-32">[</span><span class="n">s</span><span class="o">.</span><span class="n">min</span><span class="p">,</span><span class="w"> </span><span class="n">s</span><span class="o">.</span><span class="n">max</span><span class="p" data-group-id="7809665581-32">]</span><span class="p" data-group-id="7809665581-30">)</span><span class="w">
        </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">order_by</span><span class="p" data-group-id="7809665581-33">(</span><span class="p" data-group-id="7809665581-34">[</span><span class="n">s</span><span class="p" data-group-id="7809665581-34">]</span><span class="p">,</span><span class="w"> </span><span class="ss">asc</span><span class="p">:</span><span class="w"> </span><span class="n">s</span><span class="o">.</span><span class="n">min</span><span class="p" data-group-id="7809665581-33">)</span><span class="w">
        </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">select</span><span class="p" data-group-id="7809665581-35">(</span><span class="p" data-group-id="7809665581-36">[</span><span class="n">s</span><span class="p">,</span><span class="w"> </span><span class="n">r</span><span class="p" data-group-id="7809665581-36">]</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="7809665581-37">%{</span><span class="w">
          </span><span class="ss">min</span><span class="p">:</span><span class="w"> </span><span class="n">s</span><span class="o">.</span><span class="n">min</span><span class="p">,</span><span class="w">
          </span><span class="ss">max</span><span class="p">:</span><span class="w"> </span><span class="n">s</span><span class="o">.</span><span class="n">max</span><span class="p">,</span><span class="w">
          </span><span class="ss">count</span><span class="p">:</span><span class="w"> </span><span class="n">count</span><span class="p" data-group-id="7809665581-38">(</span><span class="n">r</span><span class="o">.</span><span class="n">id</span><span class="p" data-group-id="7809665581-38">)</span><span class="w">
        </span><span class="p" data-group-id="7809665581-37">}</span><span class="p" data-group-id="7809665581-35">)</span><span class="w">
        </span><span class="o">|&gt;</span><span class="w"> </span><span class="nc">Repo</span><span class="o">.</span><span class="n">all</span><span class="p" data-group-id="7809665581-39">(</span><span class="p" data-group-id="7809665581-39">)</span><span class="w">
    </span><span class="k" data-group-id="7809665581-12">end</span><span class="w">
  </span><span class="k" data-group-id="7809665581-3">end</span><span class="w">

  </span><span class="kd">defp</span><span class="w"> </span><span class="nf">lower_bound</span><span class="p" data-group-id="7809665581-40">(</span><span class="n">value</span><span class="p">,</span><span class="w"> </span><span class="n">step_size</span><span class="p" data-group-id="7809665581-40">)</span><span class="p">,</span><span class="w"> </span><span class="ss">do</span><span class="p">:</span><span class="w"> </span><span class="nc">Decimal</span><span class="o">.</span><span class="n">sub</span><span class="p" data-group-id="7809665581-41">(</span><span class="n">value</span><span class="p">,</span><span class="w"> </span><span class="nc">Decimal</span><span class="o">.</span><span class="n">rem</span><span class="p" data-group-id="7809665581-42">(</span><span class="n">value</span><span class="p">,</span><span class="w"> </span><span class="n">step_size</span><span class="p" data-group-id="7809665581-42">)</span><span class="p" data-group-id="7809665581-41">)</span><span class="w">

  </span><span class="kd">defp</span><span class="w"> </span><span class="nf">upper_bound</span><span class="p" data-group-id="7809665581-43">(</span><span class="n">value</span><span class="p">,</span><span class="w"> </span><span class="n">step_size</span><span class="p" data-group-id="7809665581-43">)</span><span class="p">,</span><span class="w"> </span><span class="ss">do</span><span class="p">:</span><span class="w"> </span><span class="nc">Decimal</span><span class="o">.</span><span class="n">add</span><span class="p" data-group-id="7809665581-44">(</span><span class="n">lower_bound</span><span class="p" data-group-id="7809665581-45">(</span><span class="n">value</span><span class="p">,</span><span class="w"> </span><span class="n">step_size</span><span class="p" data-group-id="7809665581-45">)</span><span class="p">,</span><span class="w"> </span><span class="n">step_size</span><span class="p" data-group-id="7809665581-44">)</span><span class="w">
</span><span class="k" data-group-id="7809665581-1">end</span></code></pre>
<details><summary class="cursor-pointer">
Tests for <code class="inline">list_record_value_distribution/1</code>.
</summary>
<pre><code class="makeup elixir"><span class="kd">defmodule</span><span class="w"> </span><span class="nc">MyApp.RecordsTest</span><span class="w"> </span><span class="k" data-group-id="7204405313-1">do</span><span class="w">
  </span><span class="kn">use</span><span class="w"> </span><span class="nc">MyApp.DataCase</span><span class="w">

  </span><span class="c1"># ...</span><span class="w">

  </span><span class="n">test</span><span class="w"> </span><span class="s">&quot;list_record_counts_over_time/1&quot;</span><span class="w"> </span><span class="k" data-group-id="7204405313-2">do</span><span class="w">
    </span><span class="n">assert</span><span class="w"> </span><span class="nc">Records</span><span class="o">.</span><span class="n">list_record_counts_over_time</span><span class="p" data-group-id="7204405313-3">(</span><span class="p" data-group-id="7204405313-4">{</span><span class="ss">:day</span><span class="p">,</span><span class="w"> </span><span class="mi">1</span><span class="p" data-group-id="7204405313-4">}</span><span class="p" data-group-id="7204405313-3">)</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="p" data-group-id="7204405313-5">[</span><span class="p" data-group-id="7204405313-5">]</span><span class="w">

    </span><span class="n">beginning_of_day</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p" data-group-id="7204405313-6">%{</span><span class="nc">DateTime</span><span class="o">.</span><span class="n">utc_now</span><span class="p" data-group-id="7204405313-7">(</span><span class="p" data-group-id="7204405313-7">)</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="ss">hour</span><span class="p">:</span><span class="w"> </span><span class="mi">0</span><span class="p">,</span><span class="w"> </span><span class="ss">minute</span><span class="p">:</span><span class="w"> </span><span class="mi">0</span><span class="p">,</span><span class="w"> </span><span class="ss">second</span><span class="p">:</span><span class="w"> </span><span class="mi">0</span><span class="p">,</span><span class="w"> </span><span class="ss">microsecond</span><span class="p">:</span><span class="w"> </span><span class="p" data-group-id="7204405313-8">{</span><span class="mi">0</span><span class="p">,</span><span class="w"> </span><span class="mi">6</span><span class="p" data-group-id="7204405313-8">}</span><span class="p" data-group-id="7204405313-6">}</span><span class="w">

    </span><span class="n">record_fixture</span><span class="p" data-group-id="7204405313-9">(</span><span class="p" data-group-id="7204405313-10">%{</span><span class="ss">inserted_at</span><span class="p">:</span><span class="w"> </span><span class="nc">DateTime</span><span class="o">.</span><span class="n">add</span><span class="p" data-group-id="7204405313-11">(</span><span class="n">beginning_of_day</span><span class="p">,</span><span class="w"> </span><span class="o">-</span><span class="mi">3</span><span class="p">,</span><span class="w"> </span><span class="ss">:day</span><span class="p" data-group-id="7204405313-11">)</span><span class="p" data-group-id="7204405313-10">}</span><span class="p" data-group-id="7204405313-9">)</span><span class="w">
    </span><span class="n">record_fixture</span><span class="p" data-group-id="7204405313-12">(</span><span class="p" data-group-id="7204405313-13">%{</span><span class="ss">inserted_at</span><span class="p">:</span><span class="w"> </span><span class="nc">DateTime</span><span class="o">.</span><span class="n">add</span><span class="p" data-group-id="7204405313-14">(</span><span class="n">beginning_of_day</span><span class="p">,</span><span class="w"> </span><span class="o">-</span><span class="mi">1</span><span class="p">,</span><span class="w"> </span><span class="ss">:day</span><span class="p" data-group-id="7204405313-14">)</span><span class="p" data-group-id="7204405313-13">}</span><span class="p" data-group-id="7204405313-12">)</span><span class="w">
    </span><span class="n">record_fixture</span><span class="p" data-group-id="7204405313-15">(</span><span class="p" data-group-id="7204405313-16">%{</span><span class="ss">inserted_at</span><span class="p">:</span><span class="w"> </span><span class="nc">DateTime</span><span class="o">.</span><span class="n">add</span><span class="p" data-group-id="7204405313-17">(</span><span class="n">beginning_of_day</span><span class="p">,</span><span class="w"> </span><span class="o">-</span><span class="mi">1</span><span class="p">,</span><span class="w"> </span><span class="ss">:hour</span><span class="p" data-group-id="7204405313-17">)</span><span class="p" data-group-id="7204405313-16">}</span><span class="p" data-group-id="7204405313-15">)</span><span class="w">
    </span><span class="n">record_fixture</span><span class="p" data-group-id="7204405313-18">(</span><span class="p" data-group-id="7204405313-19">%{</span><span class="ss">inserted_at</span><span class="p">:</span><span class="w"> </span><span class="nc">DateTime</span><span class="o">.</span><span class="n">add</span><span class="p" data-group-id="7204405313-20">(</span><span class="n">beginning_of_day</span><span class="p">,</span><span class="w"> </span><span class="o">-</span><span class="mi">1</span><span class="p">,</span><span class="w"> </span><span class="ss">:minute</span><span class="p" data-group-id="7204405313-20">)</span><span class="p" data-group-id="7204405313-19">}</span><span class="p" data-group-id="7204405313-18">)</span><span class="w">
    </span><span class="n">record_fixture</span><span class="p" data-group-id="7204405313-21">(</span><span class="p" data-group-id="7204405313-22">%{</span><span class="ss">inserted_at</span><span class="p">:</span><span class="w"> </span><span class="n">beginning_of_day</span><span class="p" data-group-id="7204405313-22">}</span><span class="p" data-group-id="7204405313-21">)</span><span class="w">

    </span><span class="n">assert</span><span class="w"> </span><span class="p" data-group-id="7204405313-23">[</span><span class="n">count_1</span><span class="p">,</span><span class="w"> </span><span class="n">count_2</span><span class="p">,</span><span class="w"> </span><span class="n">count_3</span><span class="p">,</span><span class="w"> </span><span class="n">count_4</span><span class="p" data-group-id="7204405313-23">]</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">Records</span><span class="o">.</span><span class="n">list_record_counts_over_time_2</span><span class="p" data-group-id="7204405313-24">(</span><span class="p" data-group-id="7204405313-25">{</span><span class="ss">:day</span><span class="p">,</span><span class="w"> </span><span class="mi">1</span><span class="p" data-group-id="7204405313-25">}</span><span class="p" data-group-id="7204405313-24">)</span><span class="w">
    </span><span class="n">assert</span><span class="w"> </span><span class="n">count_1</span><span class="o">.</span><span class="n">timestamp</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="nc">NaiveDateTime</span><span class="o">.</span><span class="n">beginning_of_day</span><span class="p" data-group-id="7204405313-26">(</span><span class="nc">NaiveDateTime</span><span class="o">.</span><span class="n">add</span><span class="p" data-group-id="7204405313-27">(</span><span class="n">beginning_of_day</span><span class="p">,</span><span class="w"> </span><span class="o">-</span><span class="mi">3</span><span class="p">,</span><span class="w"> </span><span class="ss">:day</span><span class="p" data-group-id="7204405313-27">)</span><span class="p" data-group-id="7204405313-26">)</span><span class="w">
    </span><span class="n">assert</span><span class="w"> </span><span class="n">count_1</span><span class="o">.</span><span class="n">count</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="mi">1</span><span class="w">
    </span><span class="n">assert</span><span class="w"> </span><span class="n">count_2</span><span class="o">.</span><span class="n">timestamp</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="nc">NaiveDateTime</span><span class="o">.</span><span class="n">beginning_of_day</span><span class="p" data-group-id="7204405313-28">(</span><span class="nc">NaiveDateTime</span><span class="o">.</span><span class="n">add</span><span class="p" data-group-id="7204405313-29">(</span><span class="n">beginning_of_day</span><span class="p">,</span><span class="w"> </span><span class="o">-</span><span class="mi">2</span><span class="p">,</span><span class="w"> </span><span class="ss">:day</span><span class="p" data-group-id="7204405313-29">)</span><span class="p" data-group-id="7204405313-28">)</span><span class="w">
    </span><span class="n">assert</span><span class="w"> </span><span class="n">count_2</span><span class="o">.</span><span class="n">count</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="mi">0</span><span class="w">
    </span><span class="n">assert</span><span class="w"> </span><span class="n">count_3</span><span class="o">.</span><span class="n">timestamp</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="nc">NaiveDateTime</span><span class="o">.</span><span class="n">beginning_of_day</span><span class="p" data-group-id="7204405313-30">(</span><span class="nc">NaiveDateTime</span><span class="o">.</span><span class="n">add</span><span class="p" data-group-id="7204405313-31">(</span><span class="n">beginning_of_day</span><span class="p">,</span><span class="w"> </span><span class="o">-</span><span class="mi">1</span><span class="p">,</span><span class="w"> </span><span class="ss">:day</span><span class="p" data-group-id="7204405313-31">)</span><span class="p" data-group-id="7204405313-30">)</span><span class="w">
    </span><span class="n">assert</span><span class="w"> </span><span class="n">count_3</span><span class="o">.</span><span class="n">count</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="mi">3</span><span class="w">
    </span><span class="n">assert</span><span class="w"> </span><span class="n">count_4</span><span class="o">.</span><span class="n">timestamp</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="nc">NaiveDateTime</span><span class="o">.</span><span class="n">beginning_of_day</span><span class="p" data-group-id="7204405313-32">(</span><span class="n">beginning_of_day</span><span class="p" data-group-id="7204405313-32">)</span><span class="w">
    </span><span class="n">assert</span><span class="w"> </span><span class="n">count_4</span><span class="o">.</span><span class="n">count</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="mi">1</span><span class="w">
  </span><span class="k" data-group-id="7204405313-2">end</span><span class="w">

  </span><span class="n">test</span><span class="w"> </span><span class="s">&quot;list_record_value_distribution/1&quot;</span><span class="w"> </span><span class="k" data-group-id="7204405313-33">do</span><span class="w">
    </span><span class="n">assert</span><span class="w"> </span><span class="nc">Records</span><span class="o">.</span><span class="n">list_record_value_distribution</span><span class="p" data-group-id="7204405313-34">(</span><span class="p" data-group-id="7204405313-34">)</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="p" data-group-id="7204405313-35">[</span><span class="p" data-group-id="7204405313-35">]</span><span class="w">

    </span><span class="k">for</span><span class="w"> </span><span class="bp">_</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="mi">1</span><span class="o">..</span><span class="mi">100</span><span class="p">,</span><span class="w"> </span><span class="ss">do</span><span class="p">:</span><span class="w"> </span><span class="n">record_fixture</span><span class="p" data-group-id="7204405313-36">(</span><span class="p" data-group-id="7204405313-37">%{</span><span class="ss">value</span><span class="p">:</span><span class="w"> </span><span class="mi">3</span><span class="p" data-group-id="7204405313-37">}</span><span class="p" data-group-id="7204405313-36">)</span><span class="w">
    </span><span class="n">record_fixture</span><span class="p" data-group-id="7204405313-38">(</span><span class="p" data-group-id="7204405313-39">%{</span><span class="ss">value</span><span class="p">:</span><span class="w"> </span><span class="nc">Decimal</span><span class="o">.</span><span class="n">new</span><span class="p" data-group-id="7204405313-40">(</span><span class="s">&quot;1.1&quot;</span><span class="p" data-group-id="7204405313-40">)</span><span class="p" data-group-id="7204405313-39">}</span><span class="p" data-group-id="7204405313-38">)</span><span class="w">
    </span><span class="n">record_fixture</span><span class="p" data-group-id="7204405313-41">(</span><span class="p" data-group-id="7204405313-42">%{</span><span class="ss">value</span><span class="p">:</span><span class="w"> </span><span class="nc">Decimal</span><span class="o">.</span><span class="n">new</span><span class="p" data-group-id="7204405313-43">(</span><span class="s">&quot;1.2&quot;</span><span class="p" data-group-id="7204405313-43">)</span><span class="p" data-group-id="7204405313-42">}</span><span class="p" data-group-id="7204405313-41">)</span><span class="w">
    </span><span class="n">record_fixture</span><span class="p" data-group-id="7204405313-44">(</span><span class="p" data-group-id="7204405313-45">%{</span><span class="ss">value</span><span class="p">:</span><span class="w"> </span><span class="mi">3</span><span class="p" data-group-id="7204405313-45">}</span><span class="p" data-group-id="7204405313-44">)</span><span class="w">
    </span><span class="n">record_fixture</span><span class="p" data-group-id="7204405313-46">(</span><span class="p" data-group-id="7204405313-47">%{</span><span class="ss">value</span><span class="p">:</span><span class="w"> </span><span class="mi">5</span><span class="p" data-group-id="7204405313-47">}</span><span class="p" data-group-id="7204405313-46">)</span><span class="w">
    </span><span class="n">record_fixture</span><span class="p" data-group-id="7204405313-48">(</span><span class="p" data-group-id="7204405313-49">%{</span><span class="ss">value</span><span class="p">:</span><span class="w"> </span><span class="nc">Decimal</span><span class="o">.</span><span class="n">new</span><span class="p" data-group-id="7204405313-50">(</span><span class="s">&quot;10.5&quot;</span><span class="p" data-group-id="7204405313-50">)</span><span class="p" data-group-id="7204405313-49">}</span><span class="p" data-group-id="7204405313-48">)</span><span class="w">
    </span><span class="n">record_fixture</span><span class="p" data-group-id="7204405313-51">(</span><span class="p" data-group-id="7204405313-52">%{</span><span class="ss">value</span><span class="p">:</span><span class="w"> </span><span class="mi">10_000</span><span class="p" data-group-id="7204405313-52">}</span><span class="p" data-group-id="7204405313-51">)</span><span class="w">

    </span><span class="p" data-group-id="7204405313-53">[</span><span class="n">value_1</span><span class="p">,</span><span class="w"> </span><span class="n">value_2</span><span class="p">,</span><span class="w"> </span><span class="n">value_3</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">rest</span><span class="p" data-group-id="7204405313-53">]</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">Records</span><span class="o">.</span><span class="n">list_record_value_distribution</span><span class="p" data-group-id="7204405313-54">(</span><span class="mi">10</span><span class="p" data-group-id="7204405313-54">)</span><span class="w">
    </span><span class="n">value_10</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">List</span><span class="o">.</span><span class="n">last</span><span class="p" data-group-id="7204405313-55">(</span><span class="n">rest</span><span class="p" data-group-id="7204405313-55">)</span><span class="w">

    </span><span class="n">assert</span><span class="w"> </span><span class="n">length</span><span class="p" data-group-id="7204405313-56">(</span><span class="n">rest</span><span class="p" data-group-id="7204405313-56">)</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="mi">7</span><span class="w">

    </span><span class="n">assert</span><span class="w"> </span><span class="n">value_1</span><span class="o">.</span><span class="n">count</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="mi">2</span><span class="w">
    </span><span class="n">assert</span><span class="w"> </span><span class="n">value_1</span><span class="o">.</span><span class="n">min</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="nc">Decimal</span><span class="o">.</span><span class="n">new</span><span class="p" data-group-id="7204405313-57">(</span><span class="s">&quot;1.0&quot;</span><span class="p" data-group-id="7204405313-57">)</span><span class="w">
    </span><span class="n">assert</span><span class="w"> </span><span class="n">value_1</span><span class="o">.</span><span class="n">max</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="nc">Decimal</span><span class="o">.</span><span class="n">new</span><span class="p" data-group-id="7204405313-58">(</span><span class="s">&quot;2.0&quot;</span><span class="p" data-group-id="7204405313-58">)</span><span class="w">

    </span><span class="n">assert</span><span class="w"> </span><span class="n">value_2</span><span class="o">.</span><span class="n">count</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="mi">0</span><span class="w">
    </span><span class="n">assert</span><span class="w"> </span><span class="n">value_2</span><span class="o">.</span><span class="n">min</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="nc">Decimal</span><span class="o">.</span><span class="n">new</span><span class="p" data-group-id="7204405313-59">(</span><span class="s">&quot;2.0&quot;</span><span class="p" data-group-id="7204405313-59">)</span><span class="w">
    </span><span class="n">assert</span><span class="w"> </span><span class="n">value_2</span><span class="o">.</span><span class="n">max</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="nc">Decimal</span><span class="o">.</span><span class="n">new</span><span class="p" data-group-id="7204405313-60">(</span><span class="s">&quot;3.0&quot;</span><span class="p" data-group-id="7204405313-60">)</span><span class="w">

    </span><span class="n">assert</span><span class="w"> </span><span class="n">value_3</span><span class="o">.</span><span class="n">count</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="mi">101</span><span class="w">
    </span><span class="n">assert</span><span class="w"> </span><span class="n">value_3</span><span class="o">.</span><span class="n">min</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="nc">Decimal</span><span class="o">.</span><span class="n">new</span><span class="p" data-group-id="7204405313-61">(</span><span class="s">&quot;3.0&quot;</span><span class="p" data-group-id="7204405313-61">)</span><span class="w">
    </span><span class="n">assert</span><span class="w"> </span><span class="n">value_3</span><span class="o">.</span><span class="n">max</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="nc">Decimal</span><span class="o">.</span><span class="n">new</span><span class="p" data-group-id="7204405313-62">(</span><span class="s">&quot;4.0&quot;</span><span class="p" data-group-id="7204405313-62">)</span><span class="w">

    </span><span class="n">assert</span><span class="w"> </span><span class="n">value_10</span><span class="o">.</span><span class="n">count</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="mi">2</span><span class="w">
    </span><span class="n">assert</span><span class="w"> </span><span class="n">value_10</span><span class="o">.</span><span class="n">min</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="nc">Decimal</span><span class="o">.</span><span class="n">new</span><span class="p" data-group-id="7204405313-63">(</span><span class="s">&quot;10.0&quot;</span><span class="p" data-group-id="7204405313-63">)</span><span class="w">
    </span><span class="n">assert</span><span class="w"> </span><span class="n">value_10</span><span class="o">.</span><span class="n">max</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="nc">Decimal</span><span class="o">.</span><span class="n">new</span><span class="p" data-group-id="7204405313-64">(</span><span class="s">&quot;10001&quot;</span><span class="p" data-group-id="7204405313-64">)</span><span class="w">
  </span><span class="k" data-group-id="7204405313-33">end</span><span class="w">
</span><span class="k" data-group-id="7204405313-1">end</span></code></pre>
</details>
<h3>
2. Fill in using Elixir</h3>
<p>
The second way is to just fill it with Elixir. Assuming the list is always sorted in ascending order you can simply iterate through it and fill in the missing data:</p>
<pre><code class="makeup elixir"><span class="kd">defmodule</span><span class="w"> </span><span class="nc">MyApp.ChartUtils</span><span class="w"> </span><span class="k" data-group-id="6118365183-1">do</span><span class="w">
  </span><span class="c1"># ...</span><span class="w">

  </span><span class="kd">def</span><span class="w"> </span><span class="nf">fill_gaps</span><span class="p" data-group-id="6118365183-2">(</span><span class="p" data-group-id="6118365183-3">[</span><span class="p" data-group-id="6118365183-3">]</span><span class="p">,</span><span class="w"> </span><span class="bp">_</span><span class="p" data-group-id="6118365183-2">)</span><span class="p">,</span><span class="w"> </span><span class="ss">do</span><span class="p">:</span><span class="w"> </span><span class="p" data-group-id="6118365183-4">[</span><span class="p" data-group-id="6118365183-4">]</span><span class="w">
  </span><span class="kd">def</span><span class="w"> </span><span class="nf">fill_gaps</span><span class="p" data-group-id="6118365183-5">(</span><span class="p" data-group-id="6118365183-6">[</span><span class="n">el</span><span class="p" data-group-id="6118365183-6">]</span><span class="p">,</span><span class="w"> </span><span class="bp">_</span><span class="p" data-group-id="6118365183-5">)</span><span class="p">,</span><span class="w"> </span><span class="ss">do</span><span class="p">:</span><span class="w"> </span><span class="p" data-group-id="6118365183-7">[</span><span class="n">el</span><span class="p" data-group-id="6118365183-7">]</span><span class="w">

  </span><span class="kd">def</span><span class="w"> </span><span class="nf">fill_gaps</span><span class="p" data-group-id="6118365183-8">(</span><span class="p" data-group-id="6118365183-9">[</span><span class="n">first</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">list</span><span class="p" data-group-id="6118365183-9">]</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="6118365183-10">{</span><span class="n">unit</span><span class="p">,</span><span class="w"> </span><span class="n">amount</span><span class="p" data-group-id="6118365183-10">}</span><span class="p" data-group-id="6118365183-8">)</span><span class="w"> </span><span class="k" data-group-id="6118365183-11">do</span><span class="w">
    </span><span class="p" data-group-id="6118365183-12">{</span><span class="n">data</span><span class="p">,</span><span class="w"> </span><span class="bp">_</span><span class="p" data-group-id="6118365183-12">}</span><span class="w"> </span><span class="o">=</span><span class="w">
      </span><span class="nc">Enum</span><span class="o">.</span><span class="n">reduce</span><span class="p" data-group-id="6118365183-13">(</span><span class="n">list</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="6118365183-14">{</span><span class="p" data-group-id="6118365183-15">[</span><span class="n">first</span><span class="p" data-group-id="6118365183-15">]</span><span class="p">,</span><span class="w"> </span><span class="n">first</span><span class="o">.</span><span class="n">timestamp</span><span class="p" data-group-id="6118365183-14">}</span><span class="p">,</span><span class="w"> </span><span class="k" data-group-id="6118365183-16">fn</span><span class="w"> </span><span class="n">data</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="6118365183-17">{</span><span class="n">acc</span><span class="p">,</span><span class="w"> </span><span class="n">prev_timestamp</span><span class="p" data-group-id="6118365183-17">}</span><span class="w"> </span><span class="o">-&gt;</span><span class="w">
        </span><span class="p" data-group-id="6118365183-18">{</span><span class="w">
          </span><span class="n">acc</span><span class="w"> </span><span class="o">++</span><span class="w"> </span><span class="n">gen_empty_range</span><span class="p" data-group-id="6118365183-19">(</span><span class="n">prev_timestamp</span><span class="p">,</span><span class="w"> </span><span class="n">data</span><span class="o">.</span><span class="n">timestamp</span><span class="p">,</span><span class="w"> </span><span class="n">unit</span><span class="p">,</span><span class="w"> </span><span class="n">amount</span><span class="p" data-group-id="6118365183-19">)</span><span class="w"> </span><span class="o">++</span><span class="w"> </span><span class="p" data-group-id="6118365183-20">[</span><span class="n">data</span><span class="p" data-group-id="6118365183-20">]</span><span class="p">,</span><span class="w">
          </span><span class="n">data</span><span class="o">.</span><span class="n">timestamp</span><span class="w">
        </span><span class="p" data-group-id="6118365183-18">}</span><span class="w">
      </span><span class="k" data-group-id="6118365183-16">end</span><span class="p" data-group-id="6118365183-13">)</span><span class="w">

    </span><span class="n">data</span><span class="w">
  </span><span class="k" data-group-id="6118365183-11">end</span><span class="w">

  </span><span class="kd">defp</span><span class="w"> </span><span class="nf">gen_empty_range</span><span class="p" data-group-id="6118365183-21">(</span><span class="n">from</span><span class="p">,</span><span class="w"> </span><span class="n">till</span><span class="p">,</span><span class="w"> </span><span class="n">unit</span><span class="p">,</span><span class="w"> </span><span class="n">amount</span><span class="p">,</span><span class="w"> </span><span class="n">acc</span><span class="w"> </span><span class="o">\\</span><span class="w"> </span><span class="p" data-group-id="6118365183-22">[</span><span class="p" data-group-id="6118365183-22">]</span><span class="p" data-group-id="6118365183-21">)</span><span class="w"> </span><span class="k" data-group-id="6118365183-23">do</span><span class="w">
    </span><span class="k">case</span><span class="w"> </span><span class="nc">NaiveDateTime</span><span class="o">.</span><span class="n">add</span><span class="p" data-group-id="6118365183-24">(</span><span class="n">from</span><span class="p">,</span><span class="w"> </span><span class="n">amount</span><span class="p">,</span><span class="w"> </span><span class="n">unit</span><span class="p" data-group-id="6118365183-24">)</span><span class="w"> </span><span class="k" data-group-id="6118365183-25">do</span><span class="w">
      </span><span class="o">^</span><span class="n">till</span><span class="w"> </span><span class="o">-&gt;</span><span class="w"> </span><span class="n">acc</span><span class="w">
      </span><span class="n">timestamp</span><span class="w"> </span><span class="o">-&gt;</span><span class="w"> </span><span class="n">acc</span><span class="w"> </span><span class="o">++</span><span class="w"> </span><span class="n">gen_empty_range</span><span class="p" data-group-id="6118365183-26">(</span><span class="n">timestamp</span><span class="p">,</span><span class="w"> </span><span class="n">till</span><span class="p">,</span><span class="w"> </span><span class="n">unit</span><span class="p">,</span><span class="w"> </span><span class="n">amount</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="6118365183-27">[</span><span class="p" data-group-id="6118365183-28">%{</span><span class="ss">timestamp</span><span class="p">:</span><span class="w"> </span><span class="n">timestamp</span><span class="p">,</span><span class="w"> </span><span class="ss">count</span><span class="p">:</span><span class="w"> </span><span class="mi">0</span><span class="p" data-group-id="6118365183-28">}</span><span class="p" data-group-id="6118365183-27">]</span><span class="p" data-group-id="6118365183-26">)</span><span class="w">
    </span><span class="k" data-group-id="6118365183-25">end</span><span class="w">
  </span><span class="k" data-group-id="6118365183-23">end</span><span class="w">
</span><span class="k" data-group-id="6118365183-1">end</span></code></pre>
<details><summary class="cursor-pointer">
Tests for <code class="inline">fill_gaps/2</code>.
</summary>
<pre><code class="makeup elixir"><span class="kd">defmodule</span><span class="w"> </span><span class="nc">MyApp.ChartUtilsTest</span><span class="w"> </span><span class="k" data-group-id="2965960329-1">do</span><span class="w">
  </span><span class="kn">use</span><span class="w"> </span><span class="nc">MyApp.DataCase</span><span class="w">

  </span><span class="c1"># ...</span><span class="w">

  </span><span class="n">test</span><span class="w"> </span><span class="s">&quot;fill_gaps/2&quot;</span><span class="w"> </span><span class="k" data-group-id="2965960329-2">do</span><span class="w">
    </span><span class="n">beginning_of_day</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">NaiveDateTime</span><span class="o">.</span><span class="n">beginning_of_day</span><span class="p" data-group-id="2965960329-3">(</span><span class="nc">DateTime</span><span class="o">.</span><span class="n">utc_now</span><span class="p" data-group-id="2965960329-4">(</span><span class="p" data-group-id="2965960329-4">)</span><span class="p" data-group-id="2965960329-3">)</span><span class="w">

    </span><span class="n">counts</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p" data-group-id="2965960329-5">[</span><span class="w">
      </span><span class="n">first</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p" data-group-id="2965960329-6">%{</span><span class="ss">timestamp</span><span class="p">:</span><span class="w"> </span><span class="nc">NaiveDateTime</span><span class="o">.</span><span class="n">add</span><span class="p" data-group-id="2965960329-7">(</span><span class="n">beginning_of_day</span><span class="p">,</span><span class="w"> </span><span class="o">-</span><span class="mi">3</span><span class="p">,</span><span class="w"> </span><span class="ss">:day</span><span class="p" data-group-id="2965960329-7">)</span><span class="p">,</span><span class="w"> </span><span class="ss">count</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="p" data-group-id="2965960329-6">}</span><span class="p">,</span><span class="w">
      </span><span class="p" data-group-id="2965960329-8">%{</span><span class="ss">timestamp</span><span class="p">:</span><span class="w"> </span><span class="nc">NaiveDateTime</span><span class="o">.</span><span class="n">add</span><span class="p" data-group-id="2965960329-9">(</span><span class="n">beginning_of_day</span><span class="p">,</span><span class="w"> </span><span class="o">-</span><span class="mi">1</span><span class="p">,</span><span class="w"> </span><span class="ss">:day</span><span class="p" data-group-id="2965960329-9">)</span><span class="p">,</span><span class="w"> </span><span class="ss">count</span><span class="p">:</span><span class="w"> </span><span class="mi">3</span><span class="p" data-group-id="2965960329-8">}</span><span class="p">,</span><span class="w">
      </span><span class="p" data-group-id="2965960329-10">%{</span><span class="ss">timestamp</span><span class="p">:</span><span class="w"> </span><span class="n">beginning_of_day</span><span class="p">,</span><span class="w"> </span><span class="ss">count</span><span class="p">:</span><span class="w"> </span><span class="mi">2</span><span class="p" data-group-id="2965960329-10">}</span><span class="w">
    </span><span class="p" data-group-id="2965960329-5">]</span><span class="w">

    </span><span class="n">assert</span><span class="w"> </span><span class="nc">ChartUtils</span><span class="o">.</span><span class="n">fill_gaps</span><span class="p" data-group-id="2965960329-11">(</span><span class="p" data-group-id="2965960329-12">[</span><span class="p" data-group-id="2965960329-12">]</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="2965960329-13">{</span><span class="ss">:day</span><span class="p">,</span><span class="w"> </span><span class="mi">1</span><span class="p" data-group-id="2965960329-13">}</span><span class="p" data-group-id="2965960329-11">)</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="p" data-group-id="2965960329-14">[</span><span class="p" data-group-id="2965960329-14">]</span><span class="w">
    </span><span class="n">assert</span><span class="w"> </span><span class="nc">ChartUtils</span><span class="o">.</span><span class="n">fill_gaps</span><span class="p" data-group-id="2965960329-15">(</span><span class="p" data-group-id="2965960329-16">[</span><span class="n">first</span><span class="p" data-group-id="2965960329-16">]</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="2965960329-17">{</span><span class="ss">:day</span><span class="p">,</span><span class="w"> </span><span class="mi">1</span><span class="p" data-group-id="2965960329-17">}</span><span class="p" data-group-id="2965960329-15">)</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="p" data-group-id="2965960329-18">[</span><span class="n">first</span><span class="p" data-group-id="2965960329-18">]</span><span class="w">
    </span><span class="n">assert</span><span class="w"> </span><span class="p" data-group-id="2965960329-19">[</span><span class="o">^</span><span class="n">first</span><span class="p">,</span><span class="w"> </span><span class="n">gap</span><span class="p">,</span><span class="w"> </span><span class="bp">_</span><span class="p">,</span><span class="w"> </span><span class="bp">_</span><span class="p" data-group-id="2965960329-19">]</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">ChartUtils</span><span class="o">.</span><span class="n">fill_gaps</span><span class="p" data-group-id="2965960329-20">(</span><span class="n">counts</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="2965960329-21">{</span><span class="ss">:day</span><span class="p">,</span><span class="w"> </span><span class="mi">1</span><span class="p" data-group-id="2965960329-21">}</span><span class="p" data-group-id="2965960329-20">)</span><span class="w">
    </span><span class="n">assert</span><span class="w"> </span><span class="n">gap</span><span class="o">.</span><span class="n">timestamp</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="nc">NaiveDateTime</span><span class="o">.</span><span class="n">add</span><span class="p" data-group-id="2965960329-22">(</span><span class="n">beginning_of_day</span><span class="p">,</span><span class="w"> </span><span class="o">-</span><span class="mi">2</span><span class="p">,</span><span class="w"> </span><span class="ss">:day</span><span class="p" data-group-id="2965960329-22">)</span><span class="w">
    </span><span class="n">assert</span><span class="w"> </span><span class="n">gap</span><span class="o">.</span><span class="n">count</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="mi">0</span><span class="w">
    </span><span class="n">assert</span><span class="w"> </span><span class="n">length</span><span class="p" data-group-id="2965960329-23">(</span><span class="nc">ChartUtils</span><span class="o">.</span><span class="n">fill_gaps</span><span class="p" data-group-id="2965960329-24">(</span><span class="n">counts</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="2965960329-25">{</span><span class="ss">:hour</span><span class="p">,</span><span class="w"> </span><span class="mi">12</span><span class="p" data-group-id="2965960329-25">}</span><span class="p" data-group-id="2965960329-24">)</span><span class="p" data-group-id="2965960329-23">)</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="mi">7</span><span class="w">
  </span><span class="k" data-group-id="2965960329-2">end</span><span class="w">
</span><span class="k" data-group-id="2965960329-1">end</span></code></pre>
</details>
<h2>
Wrap up</h2>
<p>
There are no one solution for charts, which is why you see so many charting libraries. The same goes for the dataset used for charts. The reason is that the charts/data needs context to have meaning. This post gives you ideas of how to deal with aggregation and data transformation, but you’ll have to find the best presentation for your data by experimenting.</p>
<p>
I may add more code snippets to this in the future. Happy coding!</p>

      ]]>
    </content>
  </entry>
  
  <entry>
    <title>Deleted record audit log with Ecto and PostgreSQL</title>
    <link href="https://danschultzer.com/posts/deleted-record-audit-log-with-ecto-postgresql" />
    <id>https://danschultzer.com/posts/deleted-record-audit-log-with-ecto-postgresql</id>
    <updated>2025-02-10T00:00:00Z</updated>
    <summary>How to log deletion of records using Ecto and PostgreSQL</summary>
    <content type="html">
      <![CDATA[
        <p>
José Valim wrote about how to implement <a href="https://dashbit.co/blog/soft-deletes-with-ecto">soft delete</a> using a PostgreSQL trigger. The best reason for soft delete is when you need the ability to undelete records, e.g., you give a grace period of 30 days before the record is deleted permanently.</p>
<p>
If the deletion of records is meant to be irreversible, this would be an anti-pattern. Soft delete for audit logging is problematic because you must be defensive in how you access data, and you must ensure you always use the right view or table partition.</p>
<p>
For an audit log, we want to allow the record to be deleted from the table and add a log entry to another table. This could be done entirely in your application code, but this will not catch any deletions by other applications or clients.</p>
<p>
As José writes in his blog post, the simple solution is to store a JSON blob of the data.</p>
<h2>
Deleted record audit log</h2>
<p>
For our purpose, we want to keep a paper trail of who deleted a record, when it was deleted, and why it was deleted.</p>
<p>
We’ll create a migration that creates a deleted record log table and adds a trigger on tables to insert a log entry on <code class="inline">DELETE</code> operations.</p>
<pre><code class="makeup elixir"><span class="kd">defmodule</span><span class="w"> </span><span class="nc">MyApp.Repo.Migrations.CreateDeletedRecordLogsTable</span><span class="w"> </span><span class="k" data-group-id="5343917295-1">do</span><span class="w">
  </span><span class="kn">use</span><span class="w"> </span><span class="nc">Ecto.Migration</span><span class="w">

  </span><span class="na">@trigger_on_tables</span><span class="w"> </span><span class="sx">~w(table_a table_b table_c)</span><span class="w">

  </span><span class="kd">def</span><span class="w"> </span><span class="nf">up</span><span class="w"> </span><span class="k" data-group-id="5343917295-2">do</span><span class="w">
    </span><span class="n">create</span><span class="w"> </span><span class="n">table</span><span class="p" data-group-id="5343917295-3">(</span><span class="ss">:deleted_record_logs</span><span class="p">,</span><span class="w"> </span><span class="ss">primary_key</span><span class="p">:</span><span class="w"> </span><span class="no">false</span><span class="p" data-group-id="5343917295-3">)</span><span class="w"> </span><span class="k" data-group-id="5343917295-4">do</span><span class="w">
      </span><span class="n">add</span><span class="w"> </span><span class="ss">:record_table</span><span class="p">,</span><span class="w"> </span><span class="ss">:string</span><span class="p">,</span><span class="w"> </span><span class="ss">null</span><span class="p">:</span><span class="w"> </span><span class="no">false</span><span class="p">,</span><span class="w"> </span><span class="ss">primary_key</span><span class="p">:</span><span class="w"> </span><span class="no">true</span><span class="w">
      </span><span class="n">add</span><span class="w"> </span><span class="ss">:record_id</span><span class="p">,</span><span class="w"> </span><span class="ss">:string</span><span class="p">,</span><span class="w"> </span><span class="ss">null</span><span class="p">:</span><span class="w"> </span><span class="no">false</span><span class="p">,</span><span class="w"> </span><span class="ss">primary_key</span><span class="p">:</span><span class="w"> </span><span class="no">true</span><span class="w">
      </span><span class="n">add</span><span class="w"> </span><span class="ss">:record_data</span><span class="p">,</span><span class="w"> </span><span class="ss">:jsonb</span><span class="p">,</span><span class="w"> </span><span class="ss">null</span><span class="p">:</span><span class="w"> </span><span class="no">false</span><span class="w">
      </span><span class="n">add</span><span class="w"> </span><span class="ss">:transaction_id</span><span class="p">,</span><span class="w"> </span><span class="ss">:bigint</span><span class="p">,</span><span class="w"> </span><span class="ss">null</span><span class="p">:</span><span class="w"> </span><span class="no">false</span><span class="w">
      </span><span class="n">add</span><span class="w"> </span><span class="ss">:deleted_at</span><span class="p">,</span><span class="w"> </span><span class="ss">:utc_datetime_usec</span><span class="p">,</span><span class="w"> </span><span class="ss">null</span><span class="p">:</span><span class="w"> </span><span class="no">false</span><span class="w">
      </span><span class="n">add</span><span class="w"> </span><span class="ss">:deleted_by</span><span class="p">,</span><span class="w"> </span><span class="ss">:string</span><span class="p">,</span><span class="w"> </span><span class="ss">null</span><span class="p">:</span><span class="w"> </span><span class="no">false</span><span class="w">
      </span><span class="n">add</span><span class="w"> </span><span class="ss">:delete_reason</span><span class="p">,</span><span class="w"> </span><span class="ss">:text</span><span class="p">,</span><span class="w"> </span><span class="ss">null</span><span class="p">:</span><span class="w"> </span><span class="no">false</span><span class="w">
    </span><span class="k" data-group-id="5343917295-4">end</span><span class="w">

    </span><span class="n">execute</span><span class="w"> </span><span class="s">&quot;&quot;&quot;
      CREATE OR REPLACE FUNCTION log_deleted_record()
      RETURNS TRIGGER AS $$
      BEGIN
        INSERT INTO deleted_record_logs (
          record_table,
          record_id,
          record_data,
          transaction_id,
          deleted_at,
          deleted_by,
          delete_reason
        )
        VALUES (
          TG_TABLE_NAME,
          OLD.id,
          row_to_json(OLD)::jsonb,
          txid_current(),
          NOW(),
          current_setting(&#39;app.deleted_by&#39;, true),
          current_setting(&#39;app.delete_reason&#39;, true)
        );
        RETURN OLD;
      END;
      $$ LANGUAGE plpgsql;
      &quot;&quot;&quot;</span><span class="w">

    </span><span class="k">for</span><span class="w"> </span><span class="n">table</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="na">@trigger_on_tables</span><span class="w"> </span><span class="k" data-group-id="5343917295-5">do</span><span class="w">
      </span><span class="n">execute</span><span class="w"> </span><span class="s">&quot;&quot;&quot;
        CREATE TRIGGER after_delete_trigger_</span><span class="si" data-group-id="5343917295-6">#{</span><span class="n">table</span><span class="si" data-group-id="5343917295-6">}</span><span class="s">
        AFTER DELETE ON </span><span class="si" data-group-id="5343917295-7">#{</span><span class="n">table</span><span class="si" data-group-id="5343917295-7">}</span><span class="s">
        FOR EACH ROW
        EXECUTE FUNCTION log_deleted_record();
        &quot;&quot;&quot;</span><span class="w">
    </span><span class="k" data-group-id="5343917295-5">end</span><span class="w">
  </span><span class="k" data-group-id="5343917295-2">end</span><span class="w">

  </span><span class="kd">def</span><span class="w"> </span><span class="nf">down</span><span class="w"> </span><span class="k" data-group-id="5343917295-8">do</span><span class="w">
    </span><span class="n">drop</span><span class="w"> </span><span class="n">table</span><span class="p" data-group-id="5343917295-9">(</span><span class="ss">:deleted_record_logs</span><span class="p" data-group-id="5343917295-9">)</span><span class="w">

    </span><span class="k">for</span><span class="w"> </span><span class="n">table</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="na">@trigger_on_tables</span><span class="w"> </span><span class="k" data-group-id="5343917295-10">do</span><span class="w">
      </span><span class="n">execute</span><span class="w"> </span><span class="s">&quot;DROP TRIGGER after_delete_trigger_</span><span class="si" data-group-id="5343917295-11">#{</span><span class="n">table</span><span class="si" data-group-id="5343917295-11">}</span><span class="s"> ON </span><span class="si" data-group-id="5343917295-12">#{</span><span class="n">table</span><span class="si" data-group-id="5343917295-12">}</span><span class="s">;&quot;</span><span class="w">
    </span><span class="k" data-group-id="5343917295-10">end</span><span class="w">

    </span><span class="n">execute</span><span class="w"> </span><span class="s">&quot;DROP FUNCTION log_deleted_record();&quot;</span><span class="w">
  </span><span class="k" data-group-id="5343917295-8">end</span><span class="w">
</span><span class="k" data-group-id="5343917295-1">end</span></code></pre>
<p>
A <code class="inline">log_deleted_record</code> function will trigger on deletes of records for the referenced tables and insert into the logs table:</p>
<ul>
  <li>
Record table name  </li>
  <li>
Record ID  </li>
  <li>
Record data as a JSON blob  </li>
  <li>
Transaction ID  </li>
  <li>
When it was deleted  </li>
  <li>
Who deleted it  </li>
  <li>
Why it was deleted  </li>
</ul>
<p>
The transaction ID is used to group records that were deleted within the same transaction.</p>
<p>
The <code class="inline">app.deleted_by</code> and <code class="inline">app.delete_reason</code> run-time parameters should be set in the transaction before deleting the record. We require both <code class="inline">deleted_by</code> and <code class="inline">delete_reason</code> to not be <code class="inline">NULL</code>. If these are not specified in the run-time parameters, the deletion will halt with an insert error.</p>
<p>
This is how you would delete a record:</p>
<pre><code class="makeup elixir"><span class="nc">Repo</span><span class="o">.</span><span class="n">transaction</span><span class="p" data-group-id="2484549586-1">(</span><span class="k" data-group-id="2484549586-2">fn</span><span class="w"> </span><span class="o">-&gt;</span><span class="w">
  </span><span class="nc">Repo</span><span class="o">.</span><span class="n">query!</span><span class="p" data-group-id="2484549586-3">(</span><span class="s">&quot;SET LOCAL app.deleted_by = &#39;my name&quot;</span><span class="p" data-group-id="2484549586-3">)</span><span class="w">
  </span><span class="nc">Repo</span><span class="o">.</span><span class="n">query!</span><span class="p" data-group-id="2484549586-4">(</span><span class="s">&quot;SET LOCAL app.delete_reason = &#39;invalid record&#39;&quot;</span><span class="p" data-group-id="2484549586-4">)</span><span class="w">
  </span><span class="nc">Repo</span><span class="o">.</span><span class="n">delete!</span><span class="p" data-group-id="2484549586-5">(</span><span class="n">record</span><span class="p" data-group-id="2484549586-5">)</span><span class="w">
</span><span class="k" data-group-id="2484549586-2">end</span><span class="p" data-group-id="2484549586-1">)</span></code></pre>
<p>
Using <code class="inline">SET LOCAL</code> ensures that these run-time parameters are only set for the current transaction.</p>
<h2>
Delete record context helper</h2>
<p>
To make it easier to work with, we want to set up a context function to prepare the run-time parameters for any deletes:</p>
<pre><code class="makeup elixir"><span class="kd">defmodule</span><span class="w"> </span><span class="nc">MyApp.Logs</span><span class="w"> </span><span class="k" data-group-id="5322280119-1">do</span><span class="w">
  </span><span class="na">@moduledoc</span><span class="w"> </span><span class="no">false</span><span class="w">

  </span><span class="kn">alias</span><span class="w"> </span><span class="nc">MyApp.Repo</span><span class="w">

  </span><span class="kd">def</span><span class="w"> </span><span class="nf">with_delete_record_context!</span><span class="p" data-group-id="5322280119-2">(</span><span class="n">deleted_by</span><span class="p">,</span><span class="w"> </span><span class="n">delete_reason</span><span class="p">,</span><span class="w"> </span><span class="n">callback_fn</span><span class="p" data-group-id="5322280119-2">)</span><span class="w"> </span><span class="k" data-group-id="5322280119-3">do</span><span class="w">
    </span><span class="nc">Repo</span><span class="o">.</span><span class="n">transaction</span><span class="p" data-group-id="5322280119-4">(</span><span class="k" data-group-id="5322280119-5">fn</span><span class="w"> </span><span class="o">-&gt;</span><span class="w">
      </span><span class="nc">Repo</span><span class="o">.</span><span class="n">query!</span><span class="p" data-group-id="5322280119-6">(</span><span class="s">&quot;SET LOCAL app.deleted_by = &#39;</span><span class="si" data-group-id="5322280119-7">#{</span><span class="n">escape_string</span><span class="p" data-group-id="5322280119-8">(</span><span class="n">deleted_by</span><span class="p" data-group-id="5322280119-8">)</span><span class="si" data-group-id="5322280119-7">}</span><span class="s">&#39;&quot;</span><span class="p" data-group-id="5322280119-6">)</span><span class="w">
      </span><span class="nc">Repo</span><span class="o">.</span><span class="n">query!</span><span class="p" data-group-id="5322280119-9">(</span><span class="s">&quot;SET LOCAL app.delete_reason = &#39;</span><span class="si" data-group-id="5322280119-10">#{</span><span class="n">escape_string</span><span class="p" data-group-id="5322280119-11">(</span><span class="n">delete_reason</span><span class="p" data-group-id="5322280119-11">)</span><span class="si" data-group-id="5322280119-10">}</span><span class="s">&#39;&quot;</span><span class="p" data-group-id="5322280119-9">)</span><span class="w">

      </span><span class="n">res</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">callback_fn</span><span class="o">.</span><span class="p" data-group-id="5322280119-12">(</span><span class="p" data-group-id="5322280119-12">)</span><span class="w">

      </span><span class="nc">Repo</span><span class="o">.</span><span class="n">query!</span><span class="p" data-group-id="5322280119-13">(</span><span class="s">&quot;RESET app.deleted_by&quot;</span><span class="p" data-group-id="5322280119-13">)</span><span class="w">
      </span><span class="nc">Repo</span><span class="o">.</span><span class="n">query!</span><span class="p" data-group-id="5322280119-14">(</span><span class="s">&quot;RESET app.delete_reason&quot;</span><span class="p" data-group-id="5322280119-14">)</span><span class="w">

      </span><span class="n">res</span><span class="w">
    </span><span class="k" data-group-id="5322280119-5">end</span><span class="p" data-group-id="5322280119-4">)</span><span class="w">
  </span><span class="k" data-group-id="5322280119-3">end</span><span class="w">

  </span><span class="kd">defp</span><span class="w"> </span><span class="nf">escape_string</span><span class="p" data-group-id="5322280119-15">(</span><span class="n">string</span><span class="p" data-group-id="5322280119-15">)</span><span class="w"> </span><span class="k" data-group-id="5322280119-16">do</span><span class="w">
    </span><span class="nc">:binary</span><span class="o">.</span><span class="n">replace</span><span class="p" data-group-id="5322280119-17">(</span><span class="n">string</span><span class="p">,</span><span class="w"> </span><span class="s">&quot;&#39;&quot;</span><span class="p">,</span><span class="w"> </span><span class="s">&quot;&#39;&#39;&quot;</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="5322280119-18">[</span><span class="ss">:global</span><span class="p" data-group-id="5322280119-18">]</span><span class="p" data-group-id="5322280119-17">)</span><span class="w">
  </span><span class="k" data-group-id="5322280119-16">end</span><span class="w">
</span><span class="k" data-group-id="5322280119-1">end</span></code></pre>
<p>
Now we can wrap our deletes:</p>
<pre><code class="makeup elixir"><span class="nc">MyApp.Logs</span><span class="o">.</span><span class="n">with_delete_record_context!</span><span class="p" data-group-id="7397779301-1">(</span><span class="s">&quot;myname&quot;</span><span class="p">,</span><span class="w"> </span><span class="s">&quot;invalid record&quot;</span><span class="p">,</span><span class="w"> </span><span class="k" data-group-id="7397779301-2">fn</span><span class="w"> </span><span class="o">-&gt;</span><span class="w">
  </span><span class="nc">Repo</span><span class="o">.</span><span class="n">delete!</span><span class="p" data-group-id="7397779301-3">(</span><span class="n">record_1</span><span class="p" data-group-id="7397779301-3">)</span><span class="w">
  </span><span class="nc">Repo</span><span class="o">.</span><span class="n">delete!</span><span class="p" data-group-id="7397779301-4">(</span><span class="n">record_2</span><span class="p" data-group-id="7397779301-4">)</span><span class="w">
</span><span class="k" data-group-id="7397779301-2">end</span><span class="p" data-group-id="7397779301-1">)</span></code></pre>
<h2>
Final words</h2>
<p>
With these triggers in place, we have strong guarantees for our audit log. Even if someone manually deletes one of the records, it will still be logged with <code class="inline">deleted_by</code> and <code class="inline">delete_reason</code>. I’ve rolled this out to a large production app where deletions are very consequential, and it has worked flawlessly.</p>
<details><summary class="cursor-pointer">
Tests for <code class="inline">MyApp.Logs</code>.
</summary>
<pre><code class="makeup elixir"><span class="kd">defmodule</span><span class="w"> </span><span class="nc">MyApp.LogsTest</span><span class="w"> </span><span class="k" data-group-id="2975368138-1">do</span><span class="w">
  </span><span class="kn">use</span><span class="w"> </span><span class="nc">MyApp.DataCase</span><span class="w">

  </span><span class="kn">alias</span><span class="w"> </span><span class="nc">MyApp</span><span class="o">.</span><span class="p" data-group-id="2975368138-2">{</span><span class="nc">Logs</span><span class="p">,</span><span class="w"> </span><span class="nc">Repo</span><span class="p" data-group-id="2975368138-2">}</span><span class="w">

  </span><span class="n">describe</span><span class="w"> </span><span class="s">&quot;with_delete_record_context!/2&quot;</span><span class="w"> </span><span class="k" data-group-id="2975368138-3">do</span><span class="w">
    </span><span class="na">@deleted_by</span><span class="w"> </span><span class="s">&quot;username&quot;</span><span class="w">
    </span><span class="na">@delete_reason</span><span class="w"> </span><span class="s">&quot;This was a bad entry&quot;</span><span class="w">

    </span><span class="n">test</span><span class="w"> </span><span class="s">&quot;sets context&quot;</span><span class="w"> </span><span class="k" data-group-id="2975368138-4">do</span><span class="w">
      </span><span class="n">assert</span><span class="w"> </span><span class="nc">Logs</span><span class="o">.</span><span class="n">with_delete_record_context!</span><span class="p" data-group-id="2975368138-5">(</span><span class="na">@deleted_by</span><span class="p">,</span><span class="w"> </span><span class="na">@delete_reason</span><span class="p">,</span><span class="w"> </span><span class="k" data-group-id="2975368138-6">fn</span><span class="w"> </span><span class="o">-&gt;</span><span class="w">
        </span><span class="n">assert</span><span class="w"> </span><span class="nc">Repo</span><span class="o">.</span><span class="n">query!</span><span class="p" data-group-id="2975368138-7">(</span><span class="s">&quot;SHOW app.deleted_by&quot;</span><span class="p" data-group-id="2975368138-7">)</span><span class="o">.</span><span class="n">rows</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="p" data-group-id="2975368138-8">[</span><span class="p" data-group-id="2975368138-9">[</span><span class="na">@deleted_by</span><span class="p" data-group-id="2975368138-9">]</span><span class="p" data-group-id="2975368138-8">]</span><span class="w">
        </span><span class="n">assert</span><span class="w"> </span><span class="nc">Repo</span><span class="o">.</span><span class="n">query!</span><span class="p" data-group-id="2975368138-10">(</span><span class="s">&quot;SHOW app.delete_reason&quot;</span><span class="p" data-group-id="2975368138-10">)</span><span class="o">.</span><span class="n">rows</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="p" data-group-id="2975368138-11">[</span><span class="p" data-group-id="2975368138-12">[</span><span class="na">@delete_reason</span><span class="p" data-group-id="2975368138-12">]</span><span class="p" data-group-id="2975368138-11">]</span><span class="w">

        </span><span class="ss">:ok</span><span class="w">
      </span><span class="k" data-group-id="2975368138-6">end</span><span class="p" data-group-id="2975368138-5">)</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="ss">:ok</span><span class="w">
    </span><span class="k" data-group-id="2975368138-4">end</span><span class="w">

    </span><span class="n">test</span><span class="w"> </span><span class="s">&quot;escapes&quot;</span><span class="w"> </span><span class="k" data-group-id="2975368138-13">do</span><span class="w">
      </span><span class="nc">Logs</span><span class="o">.</span><span class="n">with_delete_record_context!</span><span class="p" data-group-id="2975368138-14">(</span><span class="s">&quot;&#39;</span><span class="se">\\</span><span class="s">  &quot;</span><span class="p">,</span><span class="w"> </span><span class="na">@delete_reason</span><span class="p">,</span><span class="w"> </span><span class="k" data-group-id="2975368138-15">fn</span><span class="w"> </span><span class="o">-&gt;</span><span class="w">
        </span><span class="n">assert</span><span class="w"> </span><span class="nc">Repo</span><span class="o">.</span><span class="n">query!</span><span class="p" data-group-id="2975368138-16">(</span><span class="s">&quot;SHOW app.deleted_by&quot;</span><span class="p" data-group-id="2975368138-16">)</span><span class="o">.</span><span class="n">rows</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="p" data-group-id="2975368138-17">[</span><span class="p" data-group-id="2975368138-18">[</span><span class="s">&quot;&#39;</span><span class="se">\\</span><span class="s">  &quot;</span><span class="p" data-group-id="2975368138-18">]</span><span class="p" data-group-id="2975368138-17">]</span><span class="w">
      </span><span class="k" data-group-id="2975368138-15">end</span><span class="p" data-group-id="2975368138-14">)</span><span class="w">

      </span><span class="nc">Logs</span><span class="o">.</span><span class="n">with_delete_record_context!</span><span class="p" data-group-id="2975368138-19">(</span><span class="na">@deleted_by</span><span class="p">,</span><span class="w"> </span><span class="s">&quot;&#39;</span><span class="se">\\</span><span class="s">  &quot;</span><span class="p">,</span><span class="w"> </span><span class="k" data-group-id="2975368138-20">fn</span><span class="w"> </span><span class="o">-&gt;</span><span class="w">
        </span><span class="n">assert</span><span class="w"> </span><span class="nc">Repo</span><span class="o">.</span><span class="n">query!</span><span class="p" data-group-id="2975368138-21">(</span><span class="s">&quot;SHOW app.delete_reason&quot;</span><span class="p" data-group-id="2975368138-21">)</span><span class="o">.</span><span class="n">rows</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="p" data-group-id="2975368138-22">[</span><span class="p" data-group-id="2975368138-23">[</span><span class="s">&quot;&#39;</span><span class="se">\\</span><span class="s">  &quot;</span><span class="p" data-group-id="2975368138-23">]</span><span class="p" data-group-id="2975368138-22">]</span><span class="w">
      </span><span class="k" data-group-id="2975368138-20">end</span><span class="p" data-group-id="2975368138-19">)</span><span class="w">

      </span><span class="nc">Logs</span><span class="o">.</span><span class="n">with_delete_record_context!</span><span class="p" data-group-id="2975368138-24">(</span><span class="na">@deleted_by</span><span class="p">,</span><span class="w"> </span><span class="s">&quot;&#39;&quot;</span><span class="p">,</span><span class="w"> </span><span class="k" data-group-id="2975368138-25">fn</span><span class="w"> </span><span class="o">-&gt;</span><span class="w">
        </span><span class="n">assert</span><span class="w"> </span><span class="nc">Repo</span><span class="o">.</span><span class="n">query!</span><span class="p" data-group-id="2975368138-26">(</span><span class="s">&quot;SHOW app.delete_reason&quot;</span><span class="p" data-group-id="2975368138-26">)</span><span class="o">.</span><span class="n">rows</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="p" data-group-id="2975368138-27">[</span><span class="p" data-group-id="2975368138-28">[</span><span class="s">&quot;&#39;&quot;</span><span class="p" data-group-id="2975368138-28">]</span><span class="p" data-group-id="2975368138-27">]</span><span class="w">
      </span><span class="k" data-group-id="2975368138-25">end</span><span class="p" data-group-id="2975368138-24">)</span><span class="w">
    </span><span class="k" data-group-id="2975368138-13">end</span><span class="w">
  </span><span class="k" data-group-id="2975368138-3">end</span><span class="w">
</span><span class="k" data-group-id="2975368138-1">end</span></code></pre>
</details>

      ]]>
    </content>
  </entry>
  
  <entry>
    <title>Distributed Erlang without DNS on AWS ECS</title>
    <link href="https://danschultzer.com/posts/distributed-erlang-without-dns-in-aws-ecs" />
    <id>https://danschultzer.com/posts/distributed-erlang-without-dns-in-aws-ecs</id>
    <updated>2024-11-24T00:00:00Z</updated>
    <summary>How to connect Elixir nodes on AWS ECS using resource tags</summary>
    <content type="html">
      <![CDATA[
        <p>
Distributed Erlang is required when running multiple nodes with Phoenix LiveView and using <a href="/posts/chasing-phoenix-longpoll-reload-issue">LongPoll fallback</a>. The Phoenix code generator adds <a href="https://hex.pm/packages/dns_cluster"><code class="inline">DNSCluster</code></a> by default, which is great when you can set up DNS records. Unfortunately, in our production setup, we have a separation between the network account and the service account, which makes it impossible to set up Service Discovery (see <a href="https://github.com/aws/aws-app-mesh-examples/issues/432">this issue</a> for more).</p>
<p>
What if we simply list the private IPs of the ECS tasks using the AWS API? We could use <a href="https://hex.pm/packages/libcluster_ecs"><code class="inline">ClusterEcs</code></a>, but I prefer the simplicity of <code class="inline">DNSCluster</code>, and one issue with <code class="inline">ClusterEcs</code> is that it uses <a href="https://hex.pm/packages/ex_aws"><code class="inline">ExAWS</code></a> instead of <a href="https://hex.pm/packages/aws"><code class="inline">AWS</code></a>, which we exclusively use.</p>
<h2>
Prerequisites</h2>
<details><summary class="cursor-pointer">
You can skip this section if you have already set up your project to be ready for distributed Erlang. 
</summary>
<p>
You must allow for internode communication in the cluster. Add the following security group to your ECS tasks:</p>
<pre><code class="terraform">resource &quot;aws_security_group&quot; &quot;erlang_distribution&quot; {
  name        = &quot;${data.aws_ecs_cluster.this.cluster_name}-erlang-distribution&quot;
  description = &quot;Communication between Elixir nodes in the cluster&quot;
  vpc_id      = data.aws_vpc.this.id

  ingress {
    description = &quot;Allow distributed Erlang cluster communication&quot;
    from_port   = 0
    to_port     = 0
    protocol    = &quot;-1&quot;
    self        = true
  }
}</code></pre>
<p>
Set up an entrypoint script to pull the IP and set the release node name. I do this in the GitHub workflow that packages the Docker file:</p>
<pre><code class="yaml"># ...

jobs:
  package:
    # ...

    # It is necessary to set up an entrypoint script that sets the release node name
    # from the ECS container metadata, to enable distributed Erlang.
    - name: Set up AWS ECS bootstrap entrypoint script for distributed Erlang
      run: |
        cat &gt;&gt; .release/rel/overlays/bin/entrypoint &lt;&lt;&#39;SH&#39;
        #!/bin/sh
        set -eu
        export RELEASE_DISTRIBUTION=name
        export RELEASE_NODE=my_app-${{ github.sha }}@`curl -s $ECS_CONTAINER_METADATA_URI_V4 | jq -r &quot;.Networks[0].IPv4Addresses[0]&quot;`
        /app/bin/$1 ${2:-}
        SH

        chmod +x .release/rel/overlays/bin/entrypoint</code></pre>
<p>
Ensure that <code class="inline">curl</code> and <code class="inline">jq</code> is installed in the Docker image:</p>
<pre><code class="Dockerfile"># Start a new build stage so that the final image will only contain
# the compiled release and other runtime necessities
FROM ${RUNNER_IMAGE}
RUN apt-get update -y &amp;&amp; apt-get install -y libstdc++6 openssl libncurses5 locales \
  # For distributed Erlang it is necessary to set up an entrypoint that will pull
  # the ECS instance IP. We need curl and jq to fetch the ECS instance info.
  &amp;&amp; apt-get install -y curl jq \
  &amp;&amp; apt-get clean &amp;&amp; rm -f /var/lib/apt/lists/*_*</code></pre>
</details>
<h2>
Attaching to DNSCluster</h2>
<p>
First, configure the <code class="inline">DNSCluster</code> in <code class="inline">application.ex</code> to use the custom resolver:</p>
<pre><code class="makeup elixir"><span class="p" data-group-id="3464930515-1">{</span><span class="nc">DNSCluster</span><span class="p">,</span><span class="w"> </span><span class="ss">resolver</span><span class="p">:</span><span class="w"> </span><span class="nc">DNSCluster.ECSResolver</span><span class="p">,</span><span class="w"> </span><span class="ss">query</span><span class="p">:</span><span class="w"> </span><span class="nc">Application</span><span class="o">.</span><span class="n">get_env</span><span class="p" data-group-id="3464930515-2">(</span><span class="ss">:my_app</span><span class="p">,</span><span class="w"> </span><span class="ss">:dns_cluster_ecs_query</span><span class="p" data-group-id="3464930515-2">)</span><span class="w"> </span><span class="o">||</span><span class="w"> </span><span class="ss">:ignore</span><span class="p" data-group-id="3464930515-1">}</span></code></pre>
<p>
Now set the <code class="inline">:dns_cluster_ecs_query</code> config in <code class="inline">runtime.exs</code>:</p>
<pre><code class="makeup elixir"><span class="n">config</span><span class="w"> </span><span class="ss">:my_app</span><span class="p">,</span><span class="w"> </span><span class="ss">:dns_cluster_ecs_query</span><span class="p">,</span><span class="w"> </span><span class="nc">System</span><span class="o">.</span><span class="n">get_env</span><span class="p" data-group-id="5468376961-1">(</span><span class="s">&quot;DNS_CLUSTER_ECS_QUERY&quot;</span><span class="p" data-group-id="5468376961-1">)</span></code></pre>
<p>
Here is the awkward part of using <code class="inline">DNSCluster</code> as it expects the query option to be a binary. We’ll set the container environment as a JSON string (using Terraform here):</p>
<pre><code class="terraform"># The DNS_CLUSTER_ECS_QUERY is used for Erlang distribution for nodes to
# discover other ECS instances using `tags:GetResources`. It must contain the
# `cluster`, `region`, and `params` keys.
{
  name  = &quot;DNS_CLUSTER_ECS_QUERY&quot;
  value = jsonencode({
    &quot;cluster&quot;: data.aws_ecs_cluster.this.arn,
    &quot;region&quot;: var.region,
    &quot;params&quot;: {
      &quot;TagFilters&quot;: [
        {
          &quot;Key&quot;: &quot;Environment&quot;
          &quot;Values&quot;: [var.environment]
        },
        {
            &quot;Key&quot;: &quot;Project&quot;
            &quot;Values&quot;: [var.project]
          }
      ]
    }
  })
},</code></pre>
<p>
You should adjust the <code class="inline">TagFilters</code> to the tags you use in your project.</p>
<p>
You’ll also need to allow for <code class="inline">ecs:DescribeTasks</code> and <code class="inline">tags:GetResources</code> permissions for the task runner. You should note that <code class="inline">tags:GetResources</code> is not a resource-level permission, and you have to set it with a wildcard resource.</p>
<p>
With that out of the way, we can build the ECS resolver.</p>
<p>
The resolver will fetch the ECS resources with the AWS Resource Groups Tagging API using the provided params and then fetch the private IPv4 address using the ECS API to describe the tasks. The resolver will recursively query the API if a pagination token is returned.</p>
<pre><code class="makeup elixir"><span class="kd">defmodule</span><span class="w"> </span><span class="nc">DNSCluster.ECSResolver</span><span class="w"> </span><span class="k" data-group-id="3019248098-1">do</span><span class="w">
  </span><span class="na">@moduledoc</span><span class="w"> </span><span class="s">&quot;&quot;&quot;
  `DNSCluster` resolver for ECS.

  This resolver uses the AWS Resource Groups Tagging API to look up ECS task
  IPs. The following policies are required for the role(s) running the tasks:

  - `ecs:DescribeTasks`
  - `tags:GetResources`

  Note that `tags:*` permissions don&#39;t have resource-level permissions so you
  must set it with a wildcard resource.

  You want to start the supervisor for the `DNSCluster` application in your
  `application.ex` using this resolver:

      {DNSCluster, resolver: DNSCluster.ECSResolver, query: Application.get_env(:my_app, :dns_cluster_ecs_query) || :ignore}

  The query should be a JSON encoded binary as `DNSCluster` requires the
  argument to be a binary:

      {
        &quot;cluster&quot;: &quot;my-cluster&quot;,
        &quot;region&quot;: &quot;us-east-1&quot;,
        &quot;params&quot;: {
          &quot;TagFilters&quot;: [
            {&quot;Key&quot;: &quot;my-tag&quot;, &quot;Values&quot;: [&quot;my-tag-value&quot;]}
          ]
        }
      }

  The query must have a cluster, region, and params key. The params key is a
  map of parameters to pass to the AWS Resource Groups Tagging API.
  &quot;&quot;&quot;</span><span class="w">

  </span><span class="kn">require</span><span class="w"> </span><span class="nc">Logger</span><span class="w">

  </span><span class="kd">def</span><span class="w"> </span><span class="nf">basename</span><span class="p" data-group-id="3019248098-2">(</span><span class="n">node_name</span><span class="p" data-group-id="3019248098-2">)</span><span class="w"> </span><span class="ow">when</span><span class="w"> </span><span class="n">is_atom</span><span class="p" data-group-id="3019248098-3">(</span><span class="n">node_name</span><span class="p" data-group-id="3019248098-3">)</span><span class="w"> </span><span class="k" data-group-id="3019248098-4">do</span><span class="w">
    </span><span class="p" data-group-id="3019248098-5">[</span><span class="n">basename</span><span class="p">,</span><span class="w"> </span><span class="bp">_</span><span class="p" data-group-id="3019248098-5">]</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">String</span><span class="o">.</span><span class="n">split</span><span class="p" data-group-id="3019248098-6">(</span><span class="n">to_string</span><span class="p" data-group-id="3019248098-7">(</span><span class="n">node_name</span><span class="p" data-group-id="3019248098-7">)</span><span class="p">,</span><span class="w"> </span><span class="s">&quot;@&quot;</span><span class="p" data-group-id="3019248098-6">)</span><span class="w">
    </span><span class="n">basename</span><span class="w">
  </span><span class="k" data-group-id="3019248098-4">end</span><span class="w">

  </span><span class="kd">def</span><span class="w"> </span><span class="nf">connect_node</span><span class="p" data-group-id="3019248098-8">(</span><span class="n">node_name</span><span class="p" data-group-id="3019248098-8">)</span><span class="w"> </span><span class="ow">when</span><span class="w"> </span><span class="n">is_atom</span><span class="p" data-group-id="3019248098-9">(</span><span class="n">node_name</span><span class="p" data-group-id="3019248098-9">)</span><span class="p">,</span><span class="w"> </span><span class="ss">do</span><span class="p">:</span><span class="w"> </span><span class="nc">Node</span><span class="o">.</span><span class="n">connect</span><span class="p" data-group-id="3019248098-10">(</span><span class="n">node_name</span><span class="p" data-group-id="3019248098-10">)</span><span class="w">

  </span><span class="kd">def</span><span class="w"> </span><span class="nf">list_nodes</span><span class="p">,</span><span class="w"> </span><span class="ss">do</span><span class="p">:</span><span class="w"> </span><span class="nc">Node</span><span class="o">.</span><span class="n">list</span><span class="p" data-group-id="3019248098-11">(</span><span class="ss">:visible</span><span class="p" data-group-id="3019248098-11">)</span><span class="w">

  </span><span class="kd">def</span><span class="w"> </span><span class="nf">lookup</span><span class="p" data-group-id="3019248098-12">(</span><span class="n">query</span><span class="p">,</span><span class="w"> </span><span class="n">type</span><span class="p" data-group-id="3019248098-12">)</span><span class="w"> </span><span class="ow">when</span><span class="w"> </span><span class="n">is_binary</span><span class="p" data-group-id="3019248098-13">(</span><span class="n">query</span><span class="p" data-group-id="3019248098-13">)</span><span class="p">,</span><span class="w"> </span><span class="ss">do</span><span class="p">:</span><span class="w"> </span><span class="n">lookup</span><span class="p" data-group-id="3019248098-14">(</span><span class="nc">Jason</span><span class="o">.</span><span class="n">decode!</span><span class="p" data-group-id="3019248098-15">(</span><span class="n">query</span><span class="p" data-group-id="3019248098-15">)</span><span class="p">,</span><span class="w"> </span><span class="n">type</span><span class="p" data-group-id="3019248098-14">)</span><span class="w">

  </span><span class="kd">def</span><span class="w"> </span><span class="nf">lookup</span><span class="p" data-group-id="3019248098-16">(</span><span class="p" data-group-id="3019248098-17">%{</span><span class="s">&quot;cluster&quot;</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="n">cluster</span><span class="p">,</span><span class="w"> </span><span class="s">&quot;region&quot;</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="n">region</span><span class="p">,</span><span class="w"> </span><span class="s">&quot;params&quot;</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="n">params</span><span class="p" data-group-id="3019248098-17">}</span><span class="p">,</span><span class="w"> </span><span class="ss">:a</span><span class="p" data-group-id="3019248098-16">)</span><span class="p">,</span><span class="w"> </span><span class="ss">do</span><span class="p">:</span><span class="w"> </span><span class="n">list_task_ips</span><span class="p" data-group-id="3019248098-18">(</span><span class="n">cluster</span><span class="p">,</span><span class="w"> </span><span class="n">region</span><span class="p">,</span><span class="w"> </span><span class="n">params</span><span class="p" data-group-id="3019248098-18">)</span><span class="w">

  </span><span class="kd">def</span><span class="w"> </span><span class="nf">lookup</span><span class="p" data-group-id="3019248098-19">(</span><span class="c">_query</span><span class="p">,</span><span class="w"> </span><span class="ss">:aaaa</span><span class="p" data-group-id="3019248098-19">)</span><span class="p">,</span><span class="w"> </span><span class="ss">do</span><span class="p">:</span><span class="w"> </span><span class="p" data-group-id="3019248098-20">[</span><span class="p" data-group-id="3019248098-20">]</span><span class="w">

  </span><span class="kd">defp</span><span class="w"> </span><span class="nf">list_task_ips</span><span class="p" data-group-id="3019248098-21">(</span><span class="n">cluster</span><span class="p">,</span><span class="w"> </span><span class="n">region</span><span class="p">,</span><span class="w"> </span><span class="n">params</span><span class="p" data-group-id="3019248098-21">)</span><span class="w"> </span><span class="k" data-group-id="3019248098-22">do</span><span class="w">
    </span><span class="n">client</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">aws_client</span><span class="p" data-group-id="3019248098-23">(</span><span class="n">region</span><span class="p" data-group-id="3019248098-23">)</span><span class="w">
    </span><span class="n">params</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">Map</span><span class="o">.</span><span class="n">put</span><span class="p" data-group-id="3019248098-24">(</span><span class="n">params</span><span class="p">,</span><span class="w"> </span><span class="s">&quot;ResourceTypeFilters&quot;</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="3019248098-25">[</span><span class="s">&quot;ecs:task&quot;</span><span class="p" data-group-id="3019248098-25">]</span><span class="p" data-group-id="3019248098-24">)</span><span class="w">

    </span><span class="n">list_task_ips</span><span class="p" data-group-id="3019248098-26">(</span><span class="n">client</span><span class="p">,</span><span class="w"> </span><span class="n">cluster</span><span class="p">,</span><span class="w"> </span><span class="n">params</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="3019248098-27">[</span><span class="p" data-group-id="3019248098-27">]</span><span class="p" data-group-id="3019248098-26">)</span><span class="w">
  </span><span class="k" data-group-id="3019248098-22">end</span><span class="w">

  </span><span class="kd">defp</span><span class="w"> </span><span class="nf">list_task_ips</span><span class="p" data-group-id="3019248098-28">(</span><span class="n">client</span><span class="p">,</span><span class="w"> </span><span class="n">cluster</span><span class="p">,</span><span class="w"> </span><span class="n">params</span><span class="p">,</span><span class="w"> </span><span class="n">ips</span><span class="p" data-group-id="3019248098-28">)</span><span class="w"> </span><span class="k" data-group-id="3019248098-29">do</span><span class="w">
    </span><span class="k">case</span><span class="w"> </span><span class="n">fetch_task_ipv4_addresses</span><span class="p" data-group-id="3019248098-30">(</span><span class="n">client</span><span class="p">,</span><span class="w"> </span><span class="n">cluster</span><span class="p">,</span><span class="w"> </span><span class="n">params</span><span class="p" data-group-id="3019248098-30">)</span><span class="w"> </span><span class="k" data-group-id="3019248098-31">do</span><span class="w">
      </span><span class="p" data-group-id="3019248098-32">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="n">task_ips</span><span class="p">,</span><span class="w"> </span><span class="n">next</span><span class="p" data-group-id="3019248098-32">}</span><span class="w"> </span><span class="o">-&gt;</span><span class="w"> </span><span class="n">list_task_ips</span><span class="p" data-group-id="3019248098-33">(</span><span class="n">client</span><span class="p">,</span><span class="w"> </span><span class="n">cluster</span><span class="p">,</span><span class="w"> </span><span class="n">params</span><span class="p">,</span><span class="w"> </span><span class="n">ips</span><span class="w"> </span><span class="o">++</span><span class="w"> </span><span class="n">task_ips</span><span class="p">,</span><span class="w"> </span><span class="n">next</span><span class="p" data-group-id="3019248098-33">)</span><span class="w">
      </span><span class="ss">:error</span><span class="w"> </span><span class="o">-&gt;</span><span class="w"> </span><span class="p" data-group-id="3019248098-34">[</span><span class="p" data-group-id="3019248098-34">]</span><span class="w">
    </span><span class="k" data-group-id="3019248098-31">end</span><span class="w">
  </span><span class="k" data-group-id="3019248098-29">end</span><span class="w">

  </span><span class="kd">defp</span><span class="w"> </span><span class="nf">list_task_ips</span><span class="p" data-group-id="3019248098-35">(</span><span class="c">_client</span><span class="p">,</span><span class="w"> </span><span class="c">_cluster</span><span class="p">,</span><span class="w"> </span><span class="c">_params</span><span class="p">,</span><span class="w"> </span><span class="n">ips</span><span class="p">,</span><span class="w"> </span><span class="no">nil</span><span class="p" data-group-id="3019248098-35">)</span><span class="p">,</span><span class="w"> </span><span class="ss">do</span><span class="p">:</span><span class="w"> </span><span class="n">ips</span><span class="w">

  </span><span class="kd">defp</span><span class="w"> </span><span class="nf">list_task_ips</span><span class="p" data-group-id="3019248098-36">(</span><span class="n">client</span><span class="p">,</span><span class="w"> </span><span class="n">cluster</span><span class="p">,</span><span class="w"> </span><span class="n">params</span><span class="p">,</span><span class="w"> </span><span class="n">ips</span><span class="p">,</span><span class="w"> </span><span class="n">next</span><span class="p" data-group-id="3019248098-36">)</span><span class="w"> </span><span class="k" data-group-id="3019248098-37">do</span><span class="w">
    </span><span class="n">list_task_ips</span><span class="p" data-group-id="3019248098-38">(</span><span class="n">client</span><span class="p">,</span><span class="w"> </span><span class="n">cluster</span><span class="p">,</span><span class="w"> </span><span class="nc">Map</span><span class="o">.</span><span class="n">put</span><span class="p" data-group-id="3019248098-39">(</span><span class="n">params</span><span class="p">,</span><span class="w"> </span><span class="s">&quot;PaginationToken&quot;</span><span class="p">,</span><span class="w"> </span><span class="n">next</span><span class="p" data-group-id="3019248098-39">)</span><span class="p">,</span><span class="w"> </span><span class="n">ips</span><span class="p" data-group-id="3019248098-38">)</span><span class="w">
  </span><span class="k" data-group-id="3019248098-37">end</span><span class="w">

  </span><span class="kd">defp</span><span class="w"> </span><span class="nf">aws_client</span><span class="p" data-group-id="3019248098-40">(</span><span class="n">region</span><span class="p" data-group-id="3019248098-40">)</span><span class="w"> </span><span class="k" data-group-id="3019248098-41">do</span><span class="w">
    </span><span class="n">credentials</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">:aws_credentials</span><span class="o">.</span><span class="n">get_credentials</span><span class="p" data-group-id="3019248098-42">(</span><span class="p" data-group-id="3019248098-42">)</span><span class="w">
    </span><span class="n">access_key</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">Map</span><span class="o">.</span><span class="n">fetch!</span><span class="p" data-group-id="3019248098-43">(</span><span class="n">credentials</span><span class="p">,</span><span class="w"> </span><span class="ss">:access_key_id</span><span class="p" data-group-id="3019248098-43">)</span><span class="w">
    </span><span class="n">secret_key</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">Map</span><span class="o">.</span><span class="n">fetch!</span><span class="p" data-group-id="3019248098-44">(</span><span class="n">credentials</span><span class="p">,</span><span class="w"> </span><span class="ss">:secret_access_key</span><span class="p" data-group-id="3019248098-44">)</span><span class="w">
    </span><span class="n">session_token</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">Map</span><span class="o">.</span><span class="n">get</span><span class="p" data-group-id="3019248098-45">(</span><span class="n">credentials</span><span class="p">,</span><span class="w"> </span><span class="ss">:token</span><span class="p" data-group-id="3019248098-45">)</span><span class="w">

    </span><span class="nc">AWS.Client</span><span class="o">.</span><span class="n">create</span><span class="p" data-group-id="3019248098-46">(</span><span class="n">access_key</span><span class="p">,</span><span class="w"> </span><span class="n">secret_key</span><span class="p">,</span><span class="w"> </span><span class="n">session_token</span><span class="p">,</span><span class="w"> </span><span class="n">region</span><span class="p" data-group-id="3019248098-46">)</span><span class="w">
  </span><span class="k" data-group-id="3019248098-41">end</span><span class="w">

  </span><span class="kd">defp</span><span class="w"> </span><span class="nf">fetch_task_ipv4_addresses</span><span class="p" data-group-id="3019248098-47">(</span><span class="n">client</span><span class="p">,</span><span class="w"> </span><span class="n">cluster</span><span class="p">,</span><span class="w"> </span><span class="n">params</span><span class="p" data-group-id="3019248098-47">)</span><span class="w"> </span><span class="k" data-group-id="3019248098-48">do</span><span class="w">
    </span><span class="k">with</span><span class="w"> </span><span class="p" data-group-id="3019248098-49">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="n">task_arns</span><span class="p">,</span><span class="w"> </span><span class="n">next</span><span class="p" data-group-id="3019248098-49">}</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="n">fetch_resource_arns</span><span class="p" data-group-id="3019248098-50">(</span><span class="n">client</span><span class="p">,</span><span class="w"> </span><span class="n">params</span><span class="p" data-group-id="3019248098-50">)</span><span class="p">,</span><span class="w">
         </span><span class="p" data-group-id="3019248098-51">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="n">task_ips</span><span class="p" data-group-id="3019248098-51">}</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="n">fetch_ipv4_addresses</span><span class="p" data-group-id="3019248098-52">(</span><span class="n">client</span><span class="p">,</span><span class="w"> </span><span class="n">cluster</span><span class="p">,</span><span class="w"> </span><span class="n">task_arns</span><span class="p" data-group-id="3019248098-52">)</span><span class="w"> </span><span class="k" data-group-id="3019248098-53">do</span><span class="w">
      </span><span class="p" data-group-id="3019248098-54">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="n">task_ips</span><span class="p">,</span><span class="w"> </span><span class="n">next</span><span class="p" data-group-id="3019248098-54">}</span><span class="w">
    </span><span class="k" data-group-id="3019248098-53">end</span><span class="w">
  </span><span class="k" data-group-id="3019248098-48">end</span><span class="w">

  </span><span class="kd">defp</span><span class="w"> </span><span class="nf">fetch_resource_arns</span><span class="p" data-group-id="3019248098-55">(</span><span class="n">client</span><span class="p">,</span><span class="w"> </span><span class="n">params</span><span class="p" data-group-id="3019248098-55">)</span><span class="w"> </span><span class="k" data-group-id="3019248098-56">do</span><span class="w">
    </span><span class="k">case</span><span class="w"> </span><span class="nc">AWS.ResourceGroupsTaggingAPI</span><span class="o">.</span><span class="n">get_resources</span><span class="p" data-group-id="3019248098-57">(</span><span class="n">client</span><span class="p">,</span><span class="w"> </span><span class="n">params</span><span class="p" data-group-id="3019248098-57">)</span><span class="w"> </span><span class="k" data-group-id="3019248098-58">do</span><span class="w">
      </span><span class="p" data-group-id="3019248098-59">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="3019248098-60">%{</span><span class="s">&quot;ResourceTagMappingList&quot;</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="n">resources</span><span class="p" data-group-id="3019248098-60">}</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">resp</span><span class="p">,</span><span class="w"> </span><span class="bp">_</span><span class="p" data-group-id="3019248098-59">}</span><span class="w"> </span><span class="o">-&gt;</span><span class="w">
        </span><span class="n">task_arns</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">Enum</span><span class="o">.</span><span class="n">map</span><span class="p" data-group-id="3019248098-61">(</span><span class="n">resources</span><span class="p">,</span><span class="w"> </span><span class="o">&amp;</span><span class="nc">Map</span><span class="o">.</span><span class="n">fetch!</span><span class="p" data-group-id="3019248098-62">(</span><span class="ni">&amp;1</span><span class="p">,</span><span class="w"> </span><span class="s">&quot;ResourceARN&quot;</span><span class="p" data-group-id="3019248098-62">)</span><span class="p" data-group-id="3019248098-61">)</span><span class="w">
        </span><span class="n">pagination_token</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">Map</span><span class="o">.</span><span class="n">fetch!</span><span class="p" data-group-id="3019248098-63">(</span><span class="n">resp</span><span class="p">,</span><span class="w"> </span><span class="s">&quot;PaginationToken&quot;</span><span class="p" data-group-id="3019248098-63">)</span><span class="w">
        </span><span class="n">next</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">String</span><span class="o">.</span><span class="n">length</span><span class="p" data-group-id="3019248098-64">(</span><span class="n">pagination_token</span><span class="p" data-group-id="3019248098-64">)</span><span class="w"> </span><span class="o">&gt;</span><span class="w"> </span><span class="mi">0</span><span class="w"> </span><span class="o">&amp;&amp;</span><span class="w"> </span><span class="n">pagination_token</span><span class="w"> </span><span class="o">||</span><span class="w"> </span><span class="no">nil</span><span class="w">

        </span><span class="p" data-group-id="3019248098-65">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="n">task_arns</span><span class="p">,</span><span class="w"> </span><span class="n">next</span><span class="p" data-group-id="3019248098-65">}</span><span class="w">

      </span><span class="p" data-group-id="3019248098-66">{</span><span class="ss">:error</span><span class="p">,</span><span class="w"> </span><span class="n">error</span><span class="p" data-group-id="3019248098-66">}</span><span class="w"> </span><span class="o">-&gt;</span><span class="w">
        </span><span class="nc">Logger</span><span class="o">.</span><span class="n">warning</span><span class="p" data-group-id="3019248098-67">(</span><span class="s">&quot;Error looking up resources by tags: </span><span class="si" data-group-id="3019248098-68">#{</span><span class="n">inspect</span><span class="p" data-group-id="3019248098-69">(</span><span class="n">error</span><span class="p" data-group-id="3019248098-69">)</span><span class="si" data-group-id="3019248098-68">}</span><span class="s">&quot;</span><span class="p" data-group-id="3019248098-67">)</span><span class="w">

        </span><span class="ss">:error</span><span class="w">
    </span><span class="k" data-group-id="3019248098-58">end</span><span class="w">
  </span><span class="k" data-group-id="3019248098-56">end</span><span class="w">

  </span><span class="kd">defp</span><span class="w"> </span><span class="nf">fetch_ipv4_addresses</span><span class="p" data-group-id="3019248098-70">(</span><span class="n">client</span><span class="p">,</span><span class="w"> </span><span class="n">cluster</span><span class="p">,</span><span class="w"> </span><span class="n">task_arns</span><span class="p" data-group-id="3019248098-70">)</span><span class="w"> </span><span class="k" data-group-id="3019248098-71">do</span><span class="w">
    </span><span class="k">case</span><span class="w"> </span><span class="nc">AWS.ECS</span><span class="o">.</span><span class="n">describe_tasks</span><span class="p" data-group-id="3019248098-72">(</span><span class="n">client</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="3019248098-73">%{</span><span class="s">&quot;cluster&quot;</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="n">cluster</span><span class="p">,</span><span class="w"> </span><span class="s">&quot;tasks&quot;</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="n">task_arns</span><span class="p" data-group-id="3019248098-73">}</span><span class="p" data-group-id="3019248098-72">)</span><span class="w"> </span><span class="k" data-group-id="3019248098-74">do</span><span class="w">
      </span><span class="p" data-group-id="3019248098-75">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="n">data</span><span class="p">,</span><span class="w"> </span><span class="bp">_</span><span class="p" data-group-id="3019248098-75">}</span><span class="w"> </span><span class="o">-&gt;</span><span class="w">
        </span><span class="p" data-group-id="3019248098-76">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="n">extract_ipv4s</span><span class="p" data-group-id="3019248098-77">(</span><span class="n">data</span><span class="p" data-group-id="3019248098-77">)</span><span class="p" data-group-id="3019248098-76">}</span><span class="w">

      </span><span class="p" data-group-id="3019248098-78">{</span><span class="ss">:error</span><span class="p">,</span><span class="w"> </span><span class="n">error</span><span class="p" data-group-id="3019248098-78">}</span><span class="w"> </span><span class="o">-&gt;</span><span class="w">
        </span><span class="nc">Logger</span><span class="o">.</span><span class="n">warning</span><span class="p" data-group-id="3019248098-79">(</span><span class="s">&quot;Error looking up task IPs: </span><span class="si" data-group-id="3019248098-80">#{</span><span class="n">inspect</span><span class="p" data-group-id="3019248098-81">(</span><span class="n">error</span><span class="p" data-group-id="3019248098-81">)</span><span class="si" data-group-id="3019248098-80">}</span><span class="s">&quot;</span><span class="p" data-group-id="3019248098-79">)</span><span class="w">

        </span><span class="ss">:error</span><span class="w">
    </span><span class="k" data-group-id="3019248098-74">end</span><span class="w">
  </span><span class="k" data-group-id="3019248098-71">end</span><span class="w">

  </span><span class="kd">defp</span><span class="w"> </span><span class="nf">extract_ipv4s</span><span class="p" data-group-id="3019248098-82">(</span><span class="n">data</span><span class="p" data-group-id="3019248098-82">)</span><span class="w"> </span><span class="k" data-group-id="3019248098-83">do</span><span class="w">
    </span><span class="n">data</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="nc">Map</span><span class="o">.</span><span class="n">fetch!</span><span class="p" data-group-id="3019248098-84">(</span><span class="s">&quot;tasks&quot;</span><span class="p" data-group-id="3019248098-84">)</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="nc">Enum</span><span class="o">.</span><span class="n">flat_map</span><span class="p" data-group-id="3019248098-85">(</span><span class="o">&amp;</span><span class="nc">Map</span><span class="o">.</span><span class="n">get</span><span class="p" data-group-id="3019248098-86">(</span><span class="ni">&amp;1</span><span class="p">,</span><span class="w"> </span><span class="s">&quot;containers&quot;</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="3019248098-87">[</span><span class="p" data-group-id="3019248098-87">]</span><span class="p" data-group-id="3019248098-86">)</span><span class="p" data-group-id="3019248098-85">)</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="nc">Enum</span><span class="o">.</span><span class="n">flat_map</span><span class="p" data-group-id="3019248098-88">(</span><span class="o">&amp;</span><span class="nc">Map</span><span class="o">.</span><span class="n">get</span><span class="p" data-group-id="3019248098-89">(</span><span class="ni">&amp;1</span><span class="p">,</span><span class="w"> </span><span class="s">&quot;networkInterfaces&quot;</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="3019248098-90">[</span><span class="p" data-group-id="3019248098-90">]</span><span class="p" data-group-id="3019248098-89">)</span><span class="p" data-group-id="3019248098-88">)</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="nc">Enum</span><span class="o">.</span><span class="n">map</span><span class="p" data-group-id="3019248098-91">(</span><span class="o">&amp;</span><span class="nc">Map</span><span class="o">.</span><span class="n">get</span><span class="p" data-group-id="3019248098-92">(</span><span class="ni">&amp;1</span><span class="p">,</span><span class="w"> </span><span class="s">&quot;privateIpv4Address&quot;</span><span class="p" data-group-id="3019248098-92">)</span><span class="p" data-group-id="3019248098-91">)</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="nc">Enum</span><span class="o">.</span><span class="n">reduce</span><span class="p" data-group-id="3019248098-93">(</span><span class="p" data-group-id="3019248098-94">[</span><span class="p" data-group-id="3019248098-94">]</span><span class="p">,</span><span class="w"> </span><span class="k" data-group-id="3019248098-95">fn</span><span class="w">
      </span><span class="no">nil</span><span class="p">,</span><span class="w"> </span><span class="n">acc</span><span class="w"> </span><span class="o">-&gt;</span><span class="w">
        </span><span class="n">acc</span><span class="w">

      </span><span class="n">ip</span><span class="p">,</span><span class="w"> </span><span class="n">acc</span><span class="w"> </span><span class="o">-&gt;</span><span class="w">
        </span><span class="p" data-group-id="3019248098-96">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="n">ip</span><span class="p" data-group-id="3019248098-96">}</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">:inet</span><span class="o">.</span><span class="n">parse_address</span><span class="p" data-group-id="3019248098-97">(</span><span class="nc">String</span><span class="o">.</span><span class="n">to_charlist</span><span class="p" data-group-id="3019248098-98">(</span><span class="n">ip</span><span class="p" data-group-id="3019248098-98">)</span><span class="p" data-group-id="3019248098-97">)</span><span class="w">

        </span><span class="n">acc</span><span class="w"> </span><span class="o">++</span><span class="w"> </span><span class="p" data-group-id="3019248098-99">[</span><span class="n">ip</span><span class="p" data-group-id="3019248098-99">]</span><span class="w">
    </span><span class="k" data-group-id="3019248098-95">end</span><span class="p" data-group-id="3019248098-93">)</span><span class="w">
  </span><span class="k" data-group-id="3019248098-83">end</span><span class="w">
</span><span class="k" data-group-id="3019248098-1">end</span></code></pre>
<p>
That’s it! We got distributed Erlang without having to deal with Service Discovery.</p>
<details><summary class="cursor-pointer">
Tests for <code class="inline">DNSCluster.ECSResolver</code>.
</summary>
<p>
The following test uses <a href="https://hex.pm/packages/test_server"><code class="inline">TestServer</code></a> to mock the AWS API.</p>
<p>
Before you can run these tests you’ll need to update the <code class="inline">aws_client</code> function in <code class="inline">DNSCluster.ECSResolver</code>. I recommend setting up a helper module that can be used to generate the AWS client:</p>
<pre><code class="makeup elixir"><span class="kd">defmodule</span><span class="w"> </span><span class="nc">AWS.MyApp</span><span class="w"> </span><span class="k" data-group-id="6227542197-1">do</span><span class="w">
  </span><span class="na">@moduledoc</span><span class="w"> </span><span class="s">&quot;&quot;&quot;
  Module that contains MyApp specific helpers.
  &quot;&quot;&quot;</span><span class="w">

  </span><span class="na">@doc</span><span class="w"> </span><span class="s">&quot;&quot;&quot;
  Creates AWS client struct.
  &quot;&quot;&quot;</span><span class="w">
  </span><span class="kd">def</span><span class="w"> </span><span class="nf">aws_client</span><span class="p" data-group-id="6227542197-2">(</span><span class="n">region</span><span class="p" data-group-id="6227542197-2">)</span><span class="w"> </span><span class="k" data-group-id="6227542197-3">do</span><span class="w">
    </span><span class="n">aws_credentials</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">aws_credentials</span><span class="p" data-group-id="6227542197-4">(</span><span class="p" data-group-id="6227542197-4">)</span><span class="w">
    </span><span class="n">access_key</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">Map</span><span class="o">.</span><span class="n">fetch!</span><span class="p" data-group-id="6227542197-5">(</span><span class="n">aws_credentials</span><span class="p">,</span><span class="w"> </span><span class="ss">:access_key_id</span><span class="p" data-group-id="6227542197-5">)</span><span class="w">
    </span><span class="n">secret_key</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">Map</span><span class="o">.</span><span class="n">fetch!</span><span class="p" data-group-id="6227542197-6">(</span><span class="n">aws_credentials</span><span class="p">,</span><span class="w"> </span><span class="ss">:secret_access_key</span><span class="p" data-group-id="6227542197-6">)</span><span class="w">
    </span><span class="n">session_token</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">Map</span><span class="o">.</span><span class="n">get</span><span class="p" data-group-id="6227542197-7">(</span><span class="n">aws_credentials</span><span class="p">,</span><span class="w"> </span><span class="ss">:token</span><span class="p" data-group-id="6227542197-7">)</span><span class="w">

    </span><span class="n">access_key</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="nc">AWS.Client</span><span class="o">.</span><span class="n">create</span><span class="p" data-group-id="6227542197-8">(</span><span class="n">secret_key</span><span class="p">,</span><span class="w"> </span><span class="n">session_token</span><span class="p">,</span><span class="w"> </span><span class="n">region</span><span class="p" data-group-id="6227542197-8">)</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">put_endpoint</span><span class="p" data-group-id="6227542197-9">(</span><span class="p" data-group-id="6227542197-9">)</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="nc">Map</span><span class="o">.</span><span class="n">merge</span><span class="p" data-group-id="6227542197-10">(</span><span class="p" data-group-id="6227542197-11">%{</span><span class="ss">proto</span><span class="p">:</span><span class="w"> </span><span class="n">aws_proto</span><span class="p" data-group-id="6227542197-12">(</span><span class="p" data-group-id="6227542197-12">)</span><span class="p" data-group-id="6227542197-11">}</span><span class="p" data-group-id="6227542197-10">)</span><span class="w">
  </span><span class="k" data-group-id="6227542197-3">end</span><span class="w">

  </span><span class="c1"># In tests we don&#39;t want to require :aws_credentials to be running</span><span class="w">
  </span><span class="k">if</span><span class="w"> </span><span class="nc">Mix</span><span class="o">.</span><span class="n">env</span><span class="p" data-group-id="6227542197-13">(</span><span class="p" data-group-id="6227542197-13">)</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="ss">:test</span><span class="w"> </span><span class="k" data-group-id="6227542197-14">do</span><span class="w">
    </span><span class="kd">defp</span><span class="w"> </span><span class="nf">aws_credentials</span><span class="w"> </span><span class="k" data-group-id="6227542197-15">do</span><span class="w">
      </span><span class="nc">Application</span><span class="o">.</span><span class="n">get_env</span><span class="p" data-group-id="6227542197-16">(</span><span class="ss">:aws</span><span class="p">,</span><span class="w"> </span><span class="ss">:credentials</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="6227542197-17">%{</span><span class="ss">access_key_id</span><span class="p">:</span><span class="w"> </span><span class="s">&quot;test&quot;</span><span class="p">,</span><span class="w"> </span><span class="ss">secret_access_key</span><span class="p">:</span><span class="w"> </span><span class="s">&quot;test&quot;</span><span class="p">,</span><span class="w"> </span><span class="ss">token</span><span class="p">:</span><span class="w"> </span><span class="s">&quot;test&quot;</span><span class="p" data-group-id="6227542197-17">}</span><span class="p" data-group-id="6227542197-16">)</span><span class="w">
    </span><span class="k" data-group-id="6227542197-15">end</span><span class="w">
  </span><span class="k" data-group-id="6227542197-14">else</span><span class="w">
    </span><span class="kd">defp</span><span class="w"> </span><span class="nf">aws_credentials</span><span class="p">,</span><span class="w"> </span><span class="ss">do</span><span class="p">:</span><span class="w"> </span><span class="nc">:aws_credentials</span><span class="o">.</span><span class="n">get_credentials</span><span class="p" data-group-id="6227542197-18">(</span><span class="p" data-group-id="6227542197-18">)</span><span class="w">
  </span><span class="k" data-group-id="6227542197-14">end</span><span class="w">

  </span><span class="c1"># In tests we want to replace the whole endpoint while in prod/dev we just want to replace the root</span><span class="w">
  </span><span class="k">if</span><span class="w"> </span><span class="nc">Mix</span><span class="o">.</span><span class="n">env</span><span class="p" data-group-id="6227542197-19">(</span><span class="p" data-group-id="6227542197-19">)</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="ss">:test</span><span class="w"> </span><span class="k" data-group-id="6227542197-20">do</span><span class="w">
    </span><span class="kd">defp</span><span class="w"> </span><span class="nf">put_endpoint</span><span class="p" data-group-id="6227542197-21">(</span><span class="n">client</span><span class="p" data-group-id="6227542197-21">)</span><span class="p">,</span><span class="w"> </span><span class="ss">do</span><span class="p">:</span><span class="w"> </span><span class="nc">AWS.Client</span><span class="o">.</span><span class="n">put_endpoint</span><span class="p" data-group-id="6227542197-22">(</span><span class="n">client</span><span class="p">,</span><span class="w"> </span><span class="n">aws_endpoint</span><span class="p" data-group-id="6227542197-23">(</span><span class="p" data-group-id="6227542197-23">)</span><span class="p" data-group-id="6227542197-22">)</span><span class="w">
  </span><span class="k" data-group-id="6227542197-20">else</span><span class="w">
    </span><span class="kd">defp</span><span class="w"> </span><span class="nf">put_endpoint</span><span class="p" data-group-id="6227542197-24">(</span><span class="n">client</span><span class="p" data-group-id="6227542197-24">)</span><span class="p">,</span><span class="w"> </span><span class="ss">do</span><span class="p">:</span><span class="w"> </span><span class="nc">AWS.Client</span><span class="o">.</span><span class="n">put_endpoint</span><span class="p" data-group-id="6227542197-25">(</span><span class="n">client</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="6227542197-26">{</span><span class="ss">:keep_prefixes</span><span class="p">,</span><span class="w"> </span><span class="n">aws_endpoint</span><span class="p" data-group-id="6227542197-27">(</span><span class="p" data-group-id="6227542197-27">)</span><span class="p" data-group-id="6227542197-26">}</span><span class="p" data-group-id="6227542197-25">)</span><span class="w">
  </span><span class="k" data-group-id="6227542197-20">end</span><span class="w">

  </span><span class="kd">defp</span><span class="w"> </span><span class="nf">aws_endpoint</span><span class="p">,</span><span class="w"> </span><span class="ss">do</span><span class="p">:</span><span class="w"> </span><span class="nc">Application</span><span class="o">.</span><span class="n">get_env</span><span class="p" data-group-id="6227542197-28">(</span><span class="ss">:aws</span><span class="p">,</span><span class="w"> </span><span class="ss">:endpoint</span><span class="p">,</span><span class="w"> </span><span class="nc">AWS.Client</span><span class="o">.</span><span class="n">default_endpoint</span><span class="p" data-group-id="6227542197-29">(</span><span class="p" data-group-id="6227542197-29">)</span><span class="p" data-group-id="6227542197-28">)</span><span class="w">

  </span><span class="kd">defp</span><span class="w"> </span><span class="nf">aws_proto</span><span class="p">,</span><span class="w"> </span><span class="ss">do</span><span class="p">:</span><span class="w"> </span><span class="nc">Application</span><span class="o">.</span><span class="n">get_env</span><span class="p" data-group-id="6227542197-30">(</span><span class="ss">:aws</span><span class="p">,</span><span class="w"> </span><span class="ss">:proto</span><span class="p">,</span><span class="w"> </span><span class="s">&quot;https&quot;</span><span class="p" data-group-id="6227542197-30">)</span><span class="w">
</span><span class="k" data-group-id="6227542197-1">end</span></code></pre>
<p>
We don’t want to start <code class="inline">:aws_credentials</code> in tests:</p>
<pre><code class="makeup elixir"><span class="c1"># mix.exs</span><span class="w">
</span><span class="kd">defmodule</span><span class="w"> </span><span class="nc">MyApp.MixProject</span><span class="w"> </span><span class="k" data-group-id="3522314394-1">do</span><span class="w">
  </span><span class="kn">use</span><span class="w"> </span><span class="nc">Mix.Project</span><span class="w">

  </span><span class="c1"># ...</span><span class="w">

  </span><span class="kd">def</span><span class="w"> </span><span class="nf">deps</span><span class="w"> </span><span class="k" data-group-id="3522314394-2">do</span><span class="w">
    </span><span class="p" data-group-id="3522314394-3">[</span><span class="w">
      </span><span class="c1"># ...</span><span class="w">
      </span><span class="p" data-group-id="3522314394-4">{</span><span class="ss">:aws</span><span class="p">,</span><span class="w"> </span><span class="s">&quot;~&gt; 1.0.2&quot;</span><span class="p" data-group-id="3522314394-4">}</span><span class="p">,</span><span class="w">
      </span><span class="c1"># We don&#39;t want `aws_credentials` to start in dev and test since</span><span class="w">
      </span><span class="c1"># the startup will fail if credentials are not available.</span><span class="w">
      </span><span class="p" data-group-id="3522314394-5">{</span><span class="ss">:aws_credentials</span><span class="p">,</span><span class="w"> </span><span class="s">&quot;~&gt; 0.3.1&quot;</span><span class="p">,</span><span class="w"> </span><span class="ss">runtime</span><span class="p">:</span><span class="w"> </span><span class="nc">Mix</span><span class="o">.</span><span class="n">env</span><span class="p" data-group-id="3522314394-6">(</span><span class="p" data-group-id="3522314394-6">)</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="ss">:prod</span><span class="p" data-group-id="3522314394-5">}</span><span class="w">
    </span><span class="p" data-group-id="3522314394-3">]</span><span class="w">
  </span><span class="k" data-group-id="3522314394-2">end</span><span class="w">

  </span><span class="c1">#...</span><span class="w">
</span><span class="k" data-group-id="3522314394-1">end</span></code></pre>
<p>
We can now test the ECSResolver (and any other functions you got that is using the <code class="inline">aws_client</code> helper function):</p>
<pre><code class="makeup elixir"><span class="kd">defmodule</span><span class="w"> </span><span class="nc">DNSCluster.ECSResolverTest</span><span class="w"> </span><span class="k" data-group-id="0369341697-1">do</span><span class="w">
  </span><span class="kn">use</span><span class="w"> </span><span class="nc">MyApp.DataCase</span><span class="w">

  </span><span class="kn">alias</span><span class="w"> </span><span class="nc">DNSCluster.ECSResolver</span><span class="w">
  </span><span class="kn">alias</span><span class="w"> </span><span class="nc">ExUnit.CaptureLog</span><span class="w">

  </span><span class="na">@params</span><span class="w"> </span><span class="s">&quot;&quot;&quot;
    {
      &quot;cluster&quot;: &quot;default&quot;,
      &quot;region&quot;: &quot;us-east-1&quot;,
      &quot;params&quot;: {
        &quot;TagFilters&quot;: [
          {
            &quot;Key&quot;: &quot;Environment&quot;,
            &quot;Values&quot;: [&quot;test&quot;]
          },
          {
            &quot;Key&quot;: &quot;Project&quot;,
            &quot;Values&quot;: [&quot;my-project&quot;]
          }
        ]
      }
    }
    &quot;&quot;&quot;</span><span class="w">

  </span><span class="na">@task_arns</span><span class="w"> </span><span class="sx">~w(arn:aws:ecs:us-east-1:012345678910:task/1 arn:aws:ecs:us-east-1:012345678910:task/2 arn:aws:ecs:us-east-1:012345678910:task/3)</span><span class="w">

  </span><span class="n">describe</span><span class="w"> </span><span class="s">&quot;lookup/2&quot;</span><span class="w"> </span><span class="k" data-group-id="0369341697-2">do</span><span class="w">
    </span><span class="n">test</span><span class="w"> </span><span class="s">&quot;with `:aaaa` type&quot;</span><span class="w"> </span><span class="k" data-group-id="0369341697-3">do</span><span class="w">
      </span><span class="n">assert</span><span class="w"> </span><span class="nc">ECSResolver</span><span class="o">.</span><span class="n">lookup</span><span class="p" data-group-id="0369341697-4">(</span><span class="na">@params</span><span class="p">,</span><span class="w"> </span><span class="ss">:aaaa</span><span class="p" data-group-id="0369341697-4">)</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="p" data-group-id="0369341697-5">[</span><span class="p" data-group-id="0369341697-5">]</span><span class="w">
    </span><span class="k" data-group-id="0369341697-3">end</span><span class="w">

    </span><span class="n">test</span><span class="w"> </span><span class="s">&quot;with `:a` type when fetching resource arns fails&quot;</span><span class="w"> </span><span class="k" data-group-id="0369341697-6">do</span><span class="w">
      </span><span class="n">start_aws_test_server</span><span class="p" data-group-id="0369341697-7">(</span><span class="p" data-group-id="0369341697-7">)</span><span class="w">
      </span><span class="n">expect_aws_post_request</span><span class="p" data-group-id="0369341697-8">(</span><span class="mi">403</span><span class="p">,</span><span class="w"> </span><span class="n">aws_api_error_fixture</span><span class="p" data-group-id="0369341697-9">(</span><span class="p" data-group-id="0369341697-9">)</span><span class="p" data-group-id="0369341697-8">)</span><span class="w">

      </span><span class="n">assert</span><span class="w"> </span><span class="nc">CaptureLog</span><span class="o">.</span><span class="n">capture_log</span><span class="p" data-group-id="0369341697-10">(</span><span class="k" data-group-id="0369341697-11">fn</span><span class="w"> </span><span class="o">-&gt;</span><span class="w">
        </span><span class="n">assert</span><span class="w"> </span><span class="nc">ECSResolver</span><span class="o">.</span><span class="n">lookup</span><span class="p" data-group-id="0369341697-12">(</span><span class="na">@params</span><span class="p">,</span><span class="w"> </span><span class="ss">:a</span><span class="p" data-group-id="0369341697-12">)</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="p" data-group-id="0369341697-13">[</span><span class="p" data-group-id="0369341697-13">]</span><span class="w">
      </span><span class="k" data-group-id="0369341697-11">end</span><span class="p" data-group-id="0369341697-10">)</span><span class="w"> </span><span class="o">=~</span><span class="w"> </span><span class="s">&quot;Error looking up resources by tags&quot;</span><span class="w">
    </span><span class="k" data-group-id="0369341697-6">end</span><span class="w">

    </span><span class="n">test</span><span class="w"> </span><span class="s">&quot;with `:a` type when describing tasks fails&quot;</span><span class="w"> </span><span class="k" data-group-id="0369341697-14">do</span><span class="w">
      </span><span class="n">start_aws_test_server</span><span class="p" data-group-id="0369341697-15">(</span><span class="p" data-group-id="0369341697-15">)</span><span class="w">
      </span><span class="n">expect_aws_post_request</span><span class="p" data-group-id="0369341697-16">(</span><span class="mi">200</span><span class="p">,</span><span class="w"> </span><span class="n">aws_resources_fixture</span><span class="p" data-group-id="0369341697-17">(</span><span class="na">@task_arns</span><span class="p" data-group-id="0369341697-17">)</span><span class="p" data-group-id="0369341697-16">)</span><span class="w">
      </span><span class="n">expect_aws_post_request</span><span class="p" data-group-id="0369341697-18">(</span><span class="mi">403</span><span class="p">,</span><span class="w"> </span><span class="n">aws_api_error_fixture</span><span class="p" data-group-id="0369341697-19">(</span><span class="p" data-group-id="0369341697-19">)</span><span class="p" data-group-id="0369341697-18">)</span><span class="w">

      </span><span class="n">assert</span><span class="w"> </span><span class="nc">CaptureLog</span><span class="o">.</span><span class="n">capture_log</span><span class="p" data-group-id="0369341697-20">(</span><span class="k" data-group-id="0369341697-21">fn</span><span class="w"> </span><span class="o">-&gt;</span><span class="w">
        </span><span class="n">assert</span><span class="w"> </span><span class="nc">ECSResolver</span><span class="o">.</span><span class="n">lookup</span><span class="p" data-group-id="0369341697-22">(</span><span class="na">@params</span><span class="p">,</span><span class="w"> </span><span class="ss">:a</span><span class="p" data-group-id="0369341697-22">)</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="p" data-group-id="0369341697-23">[</span><span class="p" data-group-id="0369341697-23">]</span><span class="w">
      </span><span class="k" data-group-id="0369341697-21">end</span><span class="p" data-group-id="0369341697-20">)</span><span class="w"> </span><span class="o">=~</span><span class="w"> </span><span class="s">&quot;Error looking up task IPs&quot;</span><span class="w">
    </span><span class="k" data-group-id="0369341697-14">end</span><span class="w">

    </span><span class="n">test</span><span class="w"> </span><span class="s">&quot;with `:a` type&quot;</span><span class="w"> </span><span class="k" data-group-id="0369341697-24">do</span><span class="w">
      </span><span class="n">start_aws_test_server</span><span class="p" data-group-id="0369341697-25">(</span><span class="p" data-group-id="0369341697-25">)</span><span class="w">

      </span><span class="n">aws_resources</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">aws_resources_fixture</span><span class="p" data-group-id="0369341697-26">(</span><span class="na">@task_arns</span><span class="p" data-group-id="0369341697-26">)</span><span class="w">

      </span><span class="n">expect_aws_post_request</span><span class="p" data-group-id="0369341697-27">(</span><span class="mi">200</span><span class="p">,</span><span class="w"> </span><span class="k" data-group-id="0369341697-28">fn</span><span class="w"> </span><span class="n">params</span><span class="w"> </span><span class="o">-&gt;</span><span class="w">
        </span><span class="n">assert</span><span class="w"> </span><span class="n">params</span><span class="p" data-group-id="0369341697-29">[</span><span class="s">&quot;ResourceTypeFilters&quot;</span><span class="p" data-group-id="0369341697-29">]</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="p" data-group-id="0369341697-30">[</span><span class="s">&quot;ecs:task&quot;</span><span class="p" data-group-id="0369341697-30">]</span><span class="w">
        </span><span class="n">assert</span><span class="w"> </span><span class="n">params</span><span class="p" data-group-id="0369341697-31">[</span><span class="s">&quot;TagFilters&quot;</span><span class="p" data-group-id="0369341697-31">]</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="nc">Jason</span><span class="o">.</span><span class="n">decode!</span><span class="p" data-group-id="0369341697-32">(</span><span class="na">@params</span><span class="p" data-group-id="0369341697-32">)</span><span class="p" data-group-id="0369341697-33">[</span><span class="s">&quot;params&quot;</span><span class="p" data-group-id="0369341697-33">]</span><span class="p" data-group-id="0369341697-34">[</span><span class="s">&quot;TagFilters&quot;</span><span class="p" data-group-id="0369341697-34">]</span><span class="w">

        </span><span class="n">aws_resources</span><span class="w">
      </span><span class="k" data-group-id="0369341697-28">end</span><span class="p" data-group-id="0369341697-27">)</span><span class="w">

      </span><span class="n">expect_aws_post_request</span><span class="p" data-group-id="0369341697-35">(</span><span class="mi">200</span><span class="p">,</span><span class="w"> </span><span class="k" data-group-id="0369341697-36">fn</span><span class="w"> </span><span class="n">params</span><span class="w"> </span><span class="o">-&gt;</span><span class="w">
        </span><span class="n">assert</span><span class="w"> </span><span class="n">params</span><span class="p" data-group-id="0369341697-37">[</span><span class="s">&quot;cluster&quot;</span><span class="p" data-group-id="0369341697-37">]</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="nc">Jason</span><span class="o">.</span><span class="n">decode!</span><span class="p" data-group-id="0369341697-38">(</span><span class="na">@params</span><span class="p" data-group-id="0369341697-38">)</span><span class="p" data-group-id="0369341697-39">[</span><span class="s">&quot;cluster&quot;</span><span class="p" data-group-id="0369341697-39">]</span><span class="w">
        </span><span class="n">assert</span><span class="w"> </span><span class="n">params</span><span class="p" data-group-id="0369341697-40">[</span><span class="s">&quot;tasks&quot;</span><span class="p" data-group-id="0369341697-40">]</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="na">@task_arns</span><span class="w">

        </span><span class="n">aws_tasks_fixture</span><span class="p" data-group-id="0369341697-41">(</span><span class="na">@task_arns</span><span class="p" data-group-id="0369341697-41">)</span><span class="w">
      </span><span class="k" data-group-id="0369341697-36">end</span><span class="p" data-group-id="0369341697-35">)</span><span class="w">

      </span><span class="n">assert</span><span class="w"> </span><span class="nc">ECSResolver</span><span class="o">.</span><span class="n">lookup</span><span class="p" data-group-id="0369341697-42">(</span><span class="na">@params</span><span class="p">,</span><span class="w"> </span><span class="ss">:a</span><span class="p" data-group-id="0369341697-42">)</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="p" data-group-id="0369341697-43">[</span><span class="p" data-group-id="0369341697-44">{</span><span class="mi">10</span><span class="p">,</span><span class="w"> </span><span class="mi">0</span><span class="p">,</span><span class="w"> </span><span class="mi">0</span><span class="p">,</span><span class="w"> </span><span class="mi">0</span><span class="p" data-group-id="0369341697-44">}</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="0369341697-45">{</span><span class="mi">10</span><span class="p">,</span><span class="w"> </span><span class="mi">0</span><span class="p">,</span><span class="w"> </span><span class="mi">0</span><span class="p">,</span><span class="w"> </span><span class="mi">1</span><span class="p" data-group-id="0369341697-45">}</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="0369341697-46">{</span><span class="mi">10</span><span class="p">,</span><span class="w"> </span><span class="mi">0</span><span class="p">,</span><span class="w"> </span><span class="mi">0</span><span class="p">,</span><span class="w"> </span><span class="mi">2</span><span class="p" data-group-id="0369341697-46">}</span><span class="p" data-group-id="0369341697-43">]</span><span class="w">
    </span><span class="k" data-group-id="0369341697-24">end</span><span class="w">

    </span><span class="n">test</span><span class="w"> </span><span class="s">&quot;with `:a` type when has nextPaginationToken&quot;</span><span class="w"> </span><span class="k" data-group-id="0369341697-47">do</span><span class="w">
      </span><span class="p" data-group-id="0369341697-48">{</span><span class="n">task_arns_1</span><span class="p">,</span><span class="w"> </span><span class="n">task_arns_2</span><span class="p" data-group-id="0369341697-48">}</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">Enum</span><span class="o">.</span><span class="n">split</span><span class="p" data-group-id="0369341697-49">(</span><span class="na">@task_arns</span><span class="p">,</span><span class="w"> </span><span class="mi">2</span><span class="p" data-group-id="0369341697-49">)</span><span class="w">

      </span><span class="n">start_aws_test_server</span><span class="p" data-group-id="0369341697-50">(</span><span class="p" data-group-id="0369341697-50">)</span><span class="w">
      </span><span class="n">expect_aws_post_request</span><span class="p" data-group-id="0369341697-51">(</span><span class="mi">200</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="0369341697-52">%{</span><span class="n">aws_resources_fixture</span><span class="p" data-group-id="0369341697-53">(</span><span class="n">task_arns_1</span><span class="p" data-group-id="0369341697-53">)</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="s">&quot;PaginationToken&quot;</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="s">&quot;nextPaginationToken&quot;</span><span class="p" data-group-id="0369341697-52">}</span><span class="p" data-group-id="0369341697-51">)</span><span class="w">
      </span><span class="n">expect_aws_post_request</span><span class="p" data-group-id="0369341697-54">(</span><span class="mi">200</span><span class="p">,</span><span class="w"> </span><span class="n">aws_tasks_fixture</span><span class="p" data-group-id="0369341697-55">(</span><span class="n">task_arns_1</span><span class="p" data-group-id="0369341697-55">)</span><span class="p" data-group-id="0369341697-54">)</span><span class="w">

      </span><span class="n">expect_aws_post_request</span><span class="p" data-group-id="0369341697-56">(</span><span class="mi">200</span><span class="p">,</span><span class="w"> </span><span class="k" data-group-id="0369341697-57">fn</span><span class="w"> </span><span class="n">params</span><span class="w"> </span><span class="o">-&gt;</span><span class="w">
        </span><span class="n">assert</span><span class="w"> </span><span class="n">params</span><span class="p" data-group-id="0369341697-58">[</span><span class="s">&quot;PaginationToken&quot;</span><span class="p" data-group-id="0369341697-58">]</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="s">&quot;nextPaginationToken&quot;</span><span class="w">

        </span><span class="n">aws_resources_fixture</span><span class="p" data-group-id="0369341697-59">(</span><span class="n">task_arns_2</span><span class="p" data-group-id="0369341697-59">)</span><span class="w">
      </span><span class="k" data-group-id="0369341697-57">end</span><span class="p" data-group-id="0369341697-56">)</span><span class="w">

      </span><span class="n">expect_aws_post_request</span><span class="p" data-group-id="0369341697-60">(</span><span class="mi">200</span><span class="p">,</span><span class="w"> </span><span class="n">aws_tasks_fixture</span><span class="p" data-group-id="0369341697-61">(</span><span class="n">task_arns_2</span><span class="p">,</span><span class="w"> </span><span class="ss">ip_start_at</span><span class="p">:</span><span class="w"> </span><span class="mi">3</span><span class="p" data-group-id="0369341697-61">)</span><span class="p" data-group-id="0369341697-60">)</span><span class="w">

      </span><span class="n">assert</span><span class="w"> </span><span class="nc">ECSResolver</span><span class="o">.</span><span class="n">lookup</span><span class="p" data-group-id="0369341697-62">(</span><span class="na">@params</span><span class="p">,</span><span class="w"> </span><span class="ss">:a</span><span class="p" data-group-id="0369341697-62">)</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="p" data-group-id="0369341697-63">[</span><span class="p" data-group-id="0369341697-64">{</span><span class="mi">10</span><span class="p">,</span><span class="w"> </span><span class="mi">0</span><span class="p">,</span><span class="w"> </span><span class="mi">0</span><span class="p">,</span><span class="w"> </span><span class="mi">0</span><span class="p" data-group-id="0369341697-64">}</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="0369341697-65">{</span><span class="mi">10</span><span class="p">,</span><span class="w"> </span><span class="mi">0</span><span class="p">,</span><span class="w"> </span><span class="mi">0</span><span class="p">,</span><span class="w"> </span><span class="mi">1</span><span class="p" data-group-id="0369341697-65">}</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="0369341697-66">{</span><span class="mi">10</span><span class="p">,</span><span class="w"> </span><span class="mi">0</span><span class="p">,</span><span class="w"> </span><span class="mi">0</span><span class="p">,</span><span class="w"> </span><span class="mi">3</span><span class="p" data-group-id="0369341697-66">}</span><span class="p" data-group-id="0369341697-63">]</span><span class="w">
    </span><span class="k" data-group-id="0369341697-47">end</span><span class="w">

    </span><span class="kd">defp</span><span class="w"> </span><span class="nf">start_aws_test_server</span><span class="w"> </span><span class="k" data-group-id="0369341697-67">do</span><span class="w">
      </span><span class="nc">TestServer</span><span class="o">.</span><span class="n">start</span><span class="p" data-group-id="0369341697-68">(</span><span class="ss">http_server</span><span class="p">:</span><span class="w"> </span><span class="p" data-group-id="0369341697-69">{</span><span class="nc">TestServer.HTTPServer.Bandit</span><span class="p">,</span><span class="w"> </span><span class="ss">http_options</span><span class="p">:</span><span class="w"> </span><span class="p" data-group-id="0369341697-70">[</span><span class="ss">compress</span><span class="p">:</span><span class="w"> </span><span class="no">false</span><span class="p" data-group-id="0369341697-70">]</span><span class="p" data-group-id="0369341697-69">}</span><span class="p" data-group-id="0369341697-68">)</span><span class="w">

      </span><span class="n">uri</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">URI</span><span class="o">.</span><span class="n">parse</span><span class="p" data-group-id="0369341697-71">(</span><span class="nc">TestServer</span><span class="o">.</span><span class="n">url</span><span class="p" data-group-id="0369341697-72">(</span><span class="p" data-group-id="0369341697-72">)</span><span class="p" data-group-id="0369341697-71">)</span><span class="w">

      </span><span class="nc">Application</span><span class="o">.</span><span class="n">put_env</span><span class="p" data-group-id="0369341697-73">(</span><span class="ss">:aws</span><span class="p">,</span><span class="w"> </span><span class="ss">:endpoint</span><span class="p">,</span><span class="w"> </span><span class="s">&quot;</span><span class="si" data-group-id="0369341697-74">#{</span><span class="n">uri</span><span class="o">.</span><span class="n">host</span><span class="si" data-group-id="0369341697-74">}</span><span class="s">:</span><span class="si" data-group-id="0369341697-75">#{</span><span class="n">uri</span><span class="o">.</span><span class="n">port</span><span class="si" data-group-id="0369341697-75">}</span><span class="s">&quot;</span><span class="p" data-group-id="0369341697-73">)</span><span class="w">
      </span><span class="nc">Application</span><span class="o">.</span><span class="n">put_env</span><span class="p" data-group-id="0369341697-76">(</span><span class="ss">:aws</span><span class="p">,</span><span class="w"> </span><span class="ss">:proto</span><span class="p">,</span><span class="w"> </span><span class="n">uri</span><span class="o">.</span><span class="n">scheme</span><span class="p" data-group-id="0369341697-76">)</span><span class="w">

      </span><span class="n">on_exit</span><span class="p" data-group-id="0369341697-77">(</span><span class="k" data-group-id="0369341697-78">fn</span><span class="w"> </span><span class="o">-&gt;</span><span class="w">
        </span><span class="nc">Application</span><span class="o">.</span><span class="n">delete_env</span><span class="p" data-group-id="0369341697-79">(</span><span class="ss">:aws</span><span class="p">,</span><span class="w"> </span><span class="ss">:endpoint</span><span class="p" data-group-id="0369341697-79">)</span><span class="w">
        </span><span class="nc">Application</span><span class="o">.</span><span class="n">delete_env</span><span class="p" data-group-id="0369341697-80">(</span><span class="ss">:aws</span><span class="p">,</span><span class="w"> </span><span class="ss">:proto</span><span class="p" data-group-id="0369341697-80">)</span><span class="w">
      </span><span class="k" data-group-id="0369341697-78">end</span><span class="p" data-group-id="0369341697-77">)</span><span class="w">
    </span><span class="k" data-group-id="0369341697-67">end</span><span class="w">

    </span><span class="kd">defp</span><span class="w"> </span><span class="nf">expect_aws_post_request</span><span class="p" data-group-id="0369341697-81">(</span><span class="n">status</span><span class="p">,</span><span class="w"> </span><span class="n">body_or_function</span><span class="p" data-group-id="0369341697-81">)</span><span class="w"> </span><span class="k" data-group-id="0369341697-82">do</span><span class="w">
      </span><span class="nc">TestServer</span><span class="o">.</span><span class="n">add</span><span class="p" data-group-id="0369341697-83">(</span><span class="s">&quot;/&quot;</span><span class="p">,</span><span class="w"> </span><span class="ss">via</span><span class="p">:</span><span class="w"> </span><span class="ss">:post</span><span class="p">,</span><span class="w"> </span><span class="ss">to</span><span class="p">:</span><span class="w"> </span><span class="k" data-group-id="0369341697-84">fn</span><span class="w"> </span><span class="n">conn</span><span class="w"> </span><span class="o">-&gt;</span><span class="w">
        </span><span class="p" data-group-id="0369341697-85">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="n">body</span><span class="p">,</span><span class="w"> </span><span class="bp">_</span><span class="p" data-group-id="0369341697-85">}</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">Plug.Conn</span><span class="o">.</span><span class="n">read_body</span><span class="p" data-group-id="0369341697-86">(</span><span class="n">conn</span><span class="p" data-group-id="0369341697-86">)</span><span class="w">
        </span><span class="n">params</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">Jason</span><span class="o">.</span><span class="n">decode!</span><span class="p" data-group-id="0369341697-87">(</span><span class="n">body</span><span class="p" data-group-id="0369341697-87">)</span><span class="w">
        </span><span class="n">body</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">is_function</span><span class="p" data-group-id="0369341697-88">(</span><span class="n">body_or_function</span><span class="p" data-group-id="0369341697-88">)</span><span class="w"> </span><span class="o">&amp;&amp;</span><span class="w"> </span><span class="n">body_or_function</span><span class="o">.</span><span class="p" data-group-id="0369341697-89">(</span><span class="n">params</span><span class="p" data-group-id="0369341697-89">)</span><span class="w"> </span><span class="o">||</span><span class="w"> </span><span class="n">body_or_function</span><span class="w">

        </span><span class="n">conn</span><span class="w">
        </span><span class="o">|&gt;</span><span class="w"> </span><span class="nc">Plug.Conn</span><span class="o">.</span><span class="n">put_resp_header</span><span class="p" data-group-id="0369341697-90">(</span><span class="s">&quot;Content-Type&quot;</span><span class="p">,</span><span class="w"> </span><span class="s">&quot;application/x-amz-json-1.1&quot;</span><span class="p" data-group-id="0369341697-90">)</span><span class="w">
        </span><span class="o">|&gt;</span><span class="w"> </span><span class="nc">Plug.Conn</span><span class="o">.</span><span class="n">send_resp</span><span class="p" data-group-id="0369341697-91">(</span><span class="n">status</span><span class="p">,</span><span class="w"> </span><span class="nc">Jason</span><span class="o">.</span><span class="n">encode!</span><span class="p" data-group-id="0369341697-92">(</span><span class="n">body</span><span class="p" data-group-id="0369341697-92">)</span><span class="p" data-group-id="0369341697-91">)</span><span class="w">
      </span><span class="k" data-group-id="0369341697-84">end</span><span class="p" data-group-id="0369341697-83">)</span><span class="w">
    </span><span class="k" data-group-id="0369341697-82">end</span><span class="w">

    </span><span class="kd">defp</span><span class="w"> </span><span class="nf">aws_api_error_fixture</span><span class="w"> </span><span class="k" data-group-id="0369341697-93">do</span><span class="w">
      </span><span class="p" data-group-id="0369341697-94">%{</span><span class="w">
        </span><span class="s">&quot;error&quot;</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="p" data-group-id="0369341697-95">%{</span><span class="w">
          </span><span class="s">&quot;code&quot;</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="s">&quot;ExpiredToken&quot;</span><span class="p">,</span><span class="w">
          </span><span class="s">&quot;message&quot;</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="s">&quot;&quot;</span><span class="p">,</span><span class="w">
          </span><span class="s">&quot;requestId&quot;</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="s">&quot;&quot;</span><span class="w">
        </span><span class="p" data-group-id="0369341697-95">}</span><span class="w">
      </span><span class="p" data-group-id="0369341697-94">}</span><span class="w">
    </span><span class="k" data-group-id="0369341697-93">end</span><span class="w">

    </span><span class="kd">defp</span><span class="w"> </span><span class="nf">aws_resources_fixture</span><span class="p" data-group-id="0369341697-96">(</span><span class="n">resource_arns</span><span class="p" data-group-id="0369341697-96">)</span><span class="w"> </span><span class="k" data-group-id="0369341697-97">do</span><span class="w">
      </span><span class="p" data-group-id="0369341697-98">%{</span><span class="w">
        </span><span class="s">&quot;PaginationToken&quot;</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="s">&quot;&quot;</span><span class="p">,</span><span class="w">
        </span><span class="s">&quot;ResourceTagMappingList&quot;</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="p" data-group-id="0369341697-99">(</span><span class="w">
          </span><span class="k">for</span><span class="w"> </span><span class="n">resource_arn</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="n">resource_arns</span><span class="w"> </span><span class="k" data-group-id="0369341697-100">do</span><span class="w">
            </span><span class="p" data-group-id="0369341697-101">%{</span><span class="w">
                </span><span class="s">&quot;ComplianceDetails&quot;</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="p" data-group-id="0369341697-102">%{</span><span class="w">
                    </span><span class="s">&quot;ComplianceStatus&quot;</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="no">true</span><span class="p">,</span><span class="w">
                    </span><span class="s">&quot;KeysWithNoncompliantValues&quot;</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="p" data-group-id="0369341697-103">[</span><span class="p" data-group-id="0369341697-103">]</span><span class="p">,</span><span class="w">
                    </span><span class="s">&quot;NoncompliantKeys&quot;</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="p" data-group-id="0369341697-104">[</span><span class="p" data-group-id="0369341697-104">]</span><span class="w">
                </span><span class="p" data-group-id="0369341697-102">}</span><span class="p">,</span><span class="w">
                </span><span class="s">&quot;ResourceARN&quot;</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="n">resource_arn</span><span class="p">,</span><span class="w">
                </span><span class="s">&quot;Tags&quot;</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="p" data-group-id="0369341697-105">[</span><span class="p" data-group-id="0369341697-105">]</span><span class="w">
            </span><span class="p" data-group-id="0369341697-101">}</span><span class="w">
          </span><span class="k" data-group-id="0369341697-100">end</span><span class="w">
        </span><span class="p" data-group-id="0369341697-99">)</span><span class="w">
      </span><span class="p" data-group-id="0369341697-98">}</span><span class="w">
    </span><span class="k" data-group-id="0369341697-97">end</span><span class="w">

    </span><span class="kd">defp</span><span class="w"> </span><span class="nf">aws_tasks_fixture</span><span class="p" data-group-id="0369341697-106">(</span><span class="n">resource_arns</span><span class="p">,</span><span class="w"> </span><span class="n">opts</span><span class="w"> </span><span class="o">\\</span><span class="w"> </span><span class="p" data-group-id="0369341697-107">[</span><span class="p" data-group-id="0369341697-107">]</span><span class="p" data-group-id="0369341697-106">)</span><span class="w"> </span><span class="k" data-group-id="0369341697-108">do</span><span class="w">
      </span><span class="p" data-group-id="0369341697-109">%{</span><span class="w">
        </span><span class="s">&quot;failures&quot;</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="p" data-group-id="0369341697-110">[</span><span class="p" data-group-id="0369341697-110">]</span><span class="p">,</span><span class="w">
        </span><span class="s">&quot;tasks&quot;</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="p" data-group-id="0369341697-111">(</span><span class="w">
          </span><span class="k">for</span><span class="w"> </span><span class="p" data-group-id="0369341697-112">{</span><span class="n">resource_arn</span><span class="p">,</span><span class="w"> </span><span class="n">n</span><span class="p" data-group-id="0369341697-112">}</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="nc">Enum</span><span class="o">.</span><span class="n">with_index</span><span class="p" data-group-id="0369341697-113">(</span><span class="n">resource_arns</span><span class="p" data-group-id="0369341697-113">)</span><span class="w"> </span><span class="k" data-group-id="0369341697-114">do</span><span class="w">
            </span><span class="p" data-group-id="0369341697-115">%{</span><span class="w">
              </span><span class="s">&quot;taskArn&quot;</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="n">resource_arn</span><span class="p">,</span><span class="w">
              </span><span class="s">&quot;overrides&quot;</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="p" data-group-id="0369341697-116">%{</span><span class="w">
                </span><span class="s">&quot;containerOverrides&quot;</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="p" data-group-id="0369341697-117">[</span><span class="w">
                  </span><span class="p" data-group-id="0369341697-118">%{</span><span class="w">
                    </span><span class="s">&quot;name&quot;</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="s">&quot;simple-app&quot;</span><span class="w">
                  </span><span class="p" data-group-id="0369341697-118">}</span><span class="p">,</span><span class="w">
                  </span><span class="p" data-group-id="0369341697-119">%{</span><span class="w">
                    </span><span class="s">&quot;name&quot;</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="s">&quot;busybox&quot;</span><span class="w">
                  </span><span class="p" data-group-id="0369341697-119">}</span><span class="w">
                </span><span class="p" data-group-id="0369341697-117">]</span><span class="w">
              </span><span class="p" data-group-id="0369341697-116">}</span><span class="p">,</span><span class="w">
              </span><span class="s">&quot;lastStatus&quot;</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="s">&quot;RUNNING&quot;</span><span class="p">,</span><span class="w">
              </span><span class="s">&quot;containerInstanceArn&quot;</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="s">&quot;arn:aws:ecs:us-east-1:012345678910:container-instance/default/5991d8da-1d59-49d2-a31f-4230f9e73140&quot;</span><span class="p">,</span><span class="w">
              </span><span class="s">&quot;createdAt&quot;</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="mf">1_476_822_811.295</span><span class="p">,</span><span class="w">
              </span><span class="s">&quot;version&quot;</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="mi">0</span><span class="p">,</span><span class="w">
              </span><span class="s">&quot;clusterArn&quot;</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="s">&quot;arn:aws:ecs:us-east-1:012345678910:cluster/default&quot;</span><span class="p">,</span><span class="w">
              </span><span class="s">&quot;startedAt&quot;</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="mf">1_476_822_833.998</span><span class="p">,</span><span class="w">
              </span><span class="s">&quot;desiredStatus&quot;</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="s">&quot;RUNNING&quot;</span><span class="p">,</span><span class="w">
              </span><span class="s">&quot;taskDefinitionArn&quot;</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="s">&quot;arn:aws:ecs:us-east-1:012345678910:task-definition/console-sample-app-dynamic-ports:1&quot;</span><span class="p">,</span><span class="w">
              </span><span class="s">&quot;startedBy&quot;</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="s">&quot;ecs-svc/9223370560032507596&quot;</span><span class="p">,</span><span class="w">
              </span><span class="s">&quot;containers&quot;</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="p" data-group-id="0369341697-120">[</span><span class="w">
                </span><span class="p" data-group-id="0369341697-121">%{</span><span class="w">
                  </span><span class="s">&quot;containerArn&quot;</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="s">&quot;arn:aws:ecs:us-east-1:012345678910:container/4df26bb4-f057-467b-a079-961675296e64&quot;</span><span class="p">,</span><span class="w">
                  </span><span class="s">&quot;taskArn&quot;</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="s">&quot;arn:aws:ecs:us-east-1:012345678910:task/default/1dc5c17a-422b-4dc4-b493-371970c6c4d6&quot;</span><span class="p">,</span><span class="w">
                  </span><span class="s">&quot;lastStatus&quot;</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="s">&quot;RUNNING&quot;</span><span class="p">,</span><span class="w">
                  </span><span class="s">&quot;name&quot;</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="s">&quot;simple-app&quot;</span><span class="p">,</span><span class="w">
                  </span><span class="s">&quot;networkBindings&quot;</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="p" data-group-id="0369341697-122">[</span><span class="w">
                    </span><span class="p" data-group-id="0369341697-123">%{</span><span class="w">
                      </span><span class="s">&quot;protocol&quot;</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="s">&quot;tcp&quot;</span><span class="p">,</span><span class="w">
                      </span><span class="s">&quot;bindIP&quot;</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="s">&quot;0.0.0.0&quot;</span><span class="p">,</span><span class="w">
                      </span><span class="s">&quot;containerPort&quot;</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="mi">80</span><span class="p">,</span><span class="w">
                      </span><span class="s">&quot;hostPort&quot;</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="mi">32_774</span><span class="w">
                    </span><span class="p" data-group-id="0369341697-123">}</span><span class="w">
                  </span><span class="p" data-group-id="0369341697-122">]</span><span class="p">,</span><span class="w">
                  </span><span class="s">&quot;networkInterfaces&quot;</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="p" data-group-id="0369341697-124">[</span><span class="w">
                      </span><span class="p" data-group-id="0369341697-125">%{</span><span class="w">
                        </span><span class="s">&quot;attachmentId&quot;</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="s">&quot;&quot;</span><span class="p">,</span><span class="w">
                        </span><span class="s">&quot;ipv6Address&quot;</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="s">&quot;&quot;</span><span class="p">,</span><span class="w">
                        </span><span class="s">&quot;privateIpv4Address&quot;</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="s">&quot;10.0.0.</span><span class="si" data-group-id="0369341697-126">#{</span><span class="nc">Keyword</span><span class="o">.</span><span class="n">get</span><span class="p" data-group-id="0369341697-127">(</span><span class="n">opts</span><span class="p">,</span><span class="w"> </span><span class="ss">:ip_start_at</span><span class="p">,</span><span class="w"> </span><span class="mi">0</span><span class="p" data-group-id="0369341697-127">)</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="n">n</span><span class="si" data-group-id="0369341697-126">}</span><span class="s">&quot;</span><span class="w">
                      </span><span class="p" data-group-id="0369341697-125">}</span><span class="w">
                  </span><span class="p" data-group-id="0369341697-124">]</span><span class="w">
                </span><span class="p" data-group-id="0369341697-121">}</span><span class="p">,</span><span class="w">
                </span><span class="p" data-group-id="0369341697-128">%{</span><span class="w">
                  </span><span class="s">&quot;containerArn&quot;</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="s">&quot;arn:aws:ecs:us-east-1:012345678910:container/e09064f7-7361-4c87-8ab9-8d073bbdbcb9&quot;</span><span class="p">,</span><span class="w">
                  </span><span class="s">&quot;taskArn&quot;</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="s">&quot;arn:aws:ecs:us-east-1:012345678910:task/default/1dc5c17a-422b-4dc4-b493-371970c6c4d6&quot;</span><span class="p">,</span><span class="w">
                  </span><span class="s">&quot;lastStatus&quot;</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="s">&quot;RUNNING&quot;</span><span class="p">,</span><span class="w">
                  </span><span class="s">&quot;name&quot;</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="s">&quot;busybox&quot;</span><span class="p">,</span><span class="w">
                  </span><span class="s">&quot;networkBindings&quot;</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="p" data-group-id="0369341697-129">[</span><span class="p" data-group-id="0369341697-129">]</span><span class="w">
                </span><span class="p" data-group-id="0369341697-128">}</span><span class="w">
              </span><span class="p" data-group-id="0369341697-120">]</span><span class="w">
            </span><span class="p" data-group-id="0369341697-115">}</span><span class="w">
          </span><span class="k" data-group-id="0369341697-114">end</span><span class="w">
        </span><span class="p" data-group-id="0369341697-111">)</span><span class="w">
      </span><span class="p" data-group-id="0369341697-109">}</span><span class="w">
    </span><span class="k" data-group-id="0369341697-108">end</span><span class="w">
  </span><span class="k" data-group-id="0369341697-2">end</span><span class="w">
</span><span class="k" data-group-id="0369341697-1">end</span><span class="w">
</span></code></pre>
</details>

      ]]>
    </content>
  </entry>
  
  <entry>
    <title>Chasing a Phoenix LiveView long poll reload issue</title>
    <link href="https://danschultzer.com/posts/chasing-phoenix-longpoll-reload-issue" />
    <id>https://danschultzer.com/posts/chasing-phoenix-longpoll-reload-issue</id>
    <updated>2024-11-03T00:00:00Z</updated>
    <summary>The reason why distribution must be set up to prevent infinite reload with Phoenix LiveView</summary>
    <content type="html">
      <![CDATA[
        <p>
<strong>TLDR;</strong> The long poll fallback in Phoenix LiveView requires distribution. All Phoenix nodes must be connected to each other.</p>
<h2>
The issue</h2>
<p>
In a Phoenix 1.7 LiveView production app, we received reports of users experiencing a reload loop. The page loads, and after 7 seconds, a flash error message with <code class="inline">Something went wrong! Hang in there while we get back on track</code> appears. The page is force reloaded, and this repeats indefinitely.</p>
<p>
The Phoenix app is running in AWS ECS, with Cloudflare in front.</p>
<h2>
Investigation</h2>
<p>
First, I had to reproduce the issue. I found out that this was related to long poll, and it was easy to reproduce by setting the session storage keys:</p>
<pre><code class="javascript">sessionStorage.setItem(&quot;phx:fallback:ge&quot;, true);
sessionStorage.setItem(&quot;phx:fallback:LongPoll&quot;, true);</code></pre>
<p>
Long poll worked just fine in our staging environment, and I couldn’t reproduce it locally either. I went through the network requests using the developer tools in Safari, Firefox, and Chrome. In all browsers, the request was just canceled. No errors, no reason, nothing.</p>
<p>
Production and staging are essentially identical. At first, we did wonder if there were some infrastructure differences that could cause odd behavior, but nothing looked out of the ordinary.</p>
<p>
It was always the same three requests, with the third being canceled. The difference between production and staging was that in staging the second request returned a JSON with <code class="inline">{&quot;status&quot;: 200}</code> while in production it was <code class="inline">{&quot;status&quot;: 410}</code>.</p>
<p>
Enabling debugging with <code class="inline">liveSocket.enableDebug()</code> showed it was a timeout error:</p>
<pre><code>[Log] phx-GAIBpoiUGJCzqWuC error: unable to join -  – {reason: &quot;timeout&quot;} (app-500773fd7d20466d8a29a0f19ba374a4.js, line 2)</code></pre>
<p>
Cloudflare confirmed that it is the browser closing the connection as it logged the request with <code class="inline">499 Client Closed Request</code> HTTP status.</p>
<p>
The issue is in Phoenix.</p>
<h2>
Phoenix.Transports.LongPoll</h2>
<p>
I looked into what could return a <code class="inline">410</code> status code. It happens when a new session is set up:</p>
<pre><code class="makeup elixir"><span class="kd">defp</span><span class="w"> </span><span class="nf">new_session</span><span class="p" data-group-id="0732067598-1">(</span><span class="n">conn</span><span class="p">,</span><span class="w"> </span><span class="n">endpoint</span><span class="p">,</span><span class="w"> </span><span class="n">handler</span><span class="p">,</span><span class="w"> </span><span class="n">opts</span><span class="p" data-group-id="0732067598-1">)</span><span class="w"> </span><span class="k" data-group-id="0732067598-2">do</span><span class="w">
  </span><span class="c1"># ...</span><span class="w">

  </span><span class="k">case</span><span class="w"> </span><span class="nc">DynamicSupervisor</span><span class="o">.</span><span class="n">start_child</span><span class="p" data-group-id="0732067598-3">(</span><span class="nc">Phoenix.Transports.LongPoll.Supervisor</span><span class="p">,</span><span class="w"> </span><span class="n">spec</span><span class="p" data-group-id="0732067598-3">)</span><span class="w"> </span><span class="k" data-group-id="0732067598-4">do</span><span class="w">
    </span><span class="ss">:ignore</span><span class="w"> </span><span class="o">-&gt;</span><span class="w">
      </span><span class="n">conn</span><span class="w"> </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">put_status</span><span class="p" data-group-id="0732067598-5">(</span><span class="ss">:forbidden</span><span class="p" data-group-id="0732067598-5">)</span><span class="w"> </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">status_json</span><span class="p" data-group-id="0732067598-6">(</span><span class="p" data-group-id="0732067598-6">)</span><span class="w">

    </span><span class="p" data-group-id="0732067598-7">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="n">server_pid</span><span class="p" data-group-id="0732067598-7">}</span><span class="w"> </span><span class="o">-&gt;</span><span class="w">
      </span><span class="n">data</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p" data-group-id="0732067598-8">{</span><span class="ss">:v1</span><span class="p">,</span><span class="w"> </span><span class="n">endpoint</span><span class="o">.</span><span class="n">config</span><span class="p" data-group-id="0732067598-9">(</span><span class="ss">:endpoint_id</span><span class="p" data-group-id="0732067598-9">)</span><span class="p">,</span><span class="w"> </span><span class="n">server_pid</span><span class="p">,</span><span class="w"> </span><span class="n">priv_topic</span><span class="p" data-group-id="0732067598-8">}</span><span class="w">
      </span><span class="n">token</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">sign_token</span><span class="p" data-group-id="0732067598-10">(</span><span class="n">endpoint</span><span class="p">,</span><span class="w"> </span><span class="n">data</span><span class="p">,</span><span class="w"> </span><span class="n">opts</span><span class="p" data-group-id="0732067598-10">)</span><span class="w">
      </span><span class="n">conn</span><span class="w"> </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">put_status</span><span class="p" data-group-id="0732067598-11">(</span><span class="ss">:gone</span><span class="p" data-group-id="0732067598-11">)</span><span class="w"> </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">status_token_messages_json</span><span class="p" data-group-id="0732067598-12">(</span><span class="n">token</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="0732067598-13">[</span><span class="p" data-group-id="0732067598-13">]</span><span class="p" data-group-id="0732067598-12">)</span><span class="w">
  </span><span class="k" data-group-id="0732067598-4">end</span><span class="w">
</span><span class="k" data-group-id="0732067598-2">end</span></code></pre>
<p>
If a previous session can’t be resumed, a new session is set up:</p>
<pre><code class="makeup elixir"><span class="kd">defp</span><span class="w"> </span><span class="nf">dispatch</span><span class="p" data-group-id="9722963345-1">(</span><span class="p" data-group-id="9722963345-2">%{</span><span class="ss">method</span><span class="p">:</span><span class="w"> </span><span class="s">&quot;GET&quot;</span><span class="p" data-group-id="9722963345-2">}</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">conn</span><span class="p">,</span><span class="w"> </span><span class="n">endpoint</span><span class="p">,</span><span class="w"> </span><span class="n">handler</span><span class="p">,</span><span class="w"> </span><span class="n">opts</span><span class="p" data-group-id="9722963345-1">)</span><span class="w"> </span><span class="k" data-group-id="9722963345-3">do</span><span class="w">
  </span><span class="k">case</span><span class="w"> </span><span class="n">resume_session</span><span class="p" data-group-id="9722963345-4">(</span><span class="n">conn</span><span class="p">,</span><span class="w"> </span><span class="n">conn</span><span class="o">.</span><span class="n">params</span><span class="p">,</span><span class="w"> </span><span class="n">endpoint</span><span class="p">,</span><span class="w"> </span><span class="n">opts</span><span class="p" data-group-id="9722963345-4">)</span><span class="w"> </span><span class="k" data-group-id="9722963345-5">do</span><span class="w">
    </span><span class="p" data-group-id="9722963345-6">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="n">new_conn</span><span class="p">,</span><span class="w"> </span><span class="n">server_ref</span><span class="p" data-group-id="9722963345-6">}</span><span class="w"> </span><span class="o">-&gt;</span><span class="w">
      </span><span class="n">listen</span><span class="p" data-group-id="9722963345-7">(</span><span class="n">new_conn</span><span class="p">,</span><span class="w"> </span><span class="n">server_ref</span><span class="p">,</span><span class="w"> </span><span class="n">endpoint</span><span class="p">,</span><span class="w"> </span><span class="n">opts</span><span class="p" data-group-id="9722963345-7">)</span><span class="w">

    </span><span class="ss">:error</span><span class="w"> </span><span class="o">-&gt;</span><span class="w">
      </span><span class="n">new_session</span><span class="p" data-group-id="9722963345-8">(</span><span class="n">conn</span><span class="p">,</span><span class="w"> </span><span class="n">endpoint</span><span class="p">,</span><span class="w"> </span><span class="n">handler</span><span class="p">,</span><span class="w"> </span><span class="n">opts</span><span class="p" data-group-id="9722963345-8">)</span><span class="w">
  </span><span class="k" data-group-id="9722963345-5">end</span><span class="w">
</span><span class="k" data-group-id="9722963345-3">end</span></code></pre>
<p>
To resume an existing session, <code class="inline">Phoenix.Transports.LongPoll</code> will try to get the server reference from the PID stored in the token and then broadcast a subscribe message on that server reference:</p>
<pre><code class="makeup elixir"><span class="kd">defp</span><span class="w"> </span><span class="nf">resume_session</span><span class="p" data-group-id="4486477585-1">(</span><span class="p" data-group-id="4486477585-2">%</span><span class="nc" data-group-id="4486477585-2">Plug.Conn</span><span class="p" data-group-id="4486477585-2">{</span><span class="p" data-group-id="4486477585-2">}</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">conn</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="4486477585-3">%{</span><span class="s">&quot;token&quot;</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="n">token</span><span class="p" data-group-id="4486477585-3">}</span><span class="p">,</span><span class="w"> </span><span class="n">endpoint</span><span class="p">,</span><span class="w"> </span><span class="n">opts</span><span class="p" data-group-id="4486477585-1">)</span><span class="w"> </span><span class="k" data-group-id="4486477585-4">do</span><span class="w">
  </span><span class="k">case</span><span class="w"> </span><span class="n">verify_token</span><span class="p" data-group-id="4486477585-5">(</span><span class="n">endpoint</span><span class="p">,</span><span class="w"> </span><span class="n">token</span><span class="p">,</span><span class="w"> </span><span class="n">opts</span><span class="p" data-group-id="4486477585-5">)</span><span class="w"> </span><span class="k" data-group-id="4486477585-6">do</span><span class="w">
    </span><span class="p" data-group-id="4486477585-7">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="4486477585-8">{</span><span class="ss">:v1</span><span class="p">,</span><span class="w"> </span><span class="n">id</span><span class="p">,</span><span class="w"> </span><span class="n">pid</span><span class="p">,</span><span class="w"> </span><span class="n">priv_topic</span><span class="p" data-group-id="4486477585-8">}</span><span class="p" data-group-id="4486477585-7">}</span><span class="w"> </span><span class="o">-&gt;</span><span class="w">
      </span><span class="n">server_ref</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">server_ref</span><span class="p" data-group-id="4486477585-9">(</span><span class="n">endpoint</span><span class="o">.</span><span class="n">config</span><span class="p" data-group-id="4486477585-10">(</span><span class="ss">:endpoint_id</span><span class="p" data-group-id="4486477585-10">)</span><span class="p">,</span><span class="w"> </span><span class="n">id</span><span class="p">,</span><span class="w"> </span><span class="n">pid</span><span class="p">,</span><span class="w"> </span><span class="n">priv_topic</span><span class="p" data-group-id="4486477585-9">)</span><span class="w">

      </span><span class="n">new_conn</span><span class="w"> </span><span class="o">=</span><span class="w">
        </span><span class="nc">Plug.Conn</span><span class="o">.</span><span class="n">register_before_send</span><span class="p" data-group-id="4486477585-11">(</span><span class="n">conn</span><span class="p">,</span><span class="w"> </span><span class="k" data-group-id="4486477585-12">fn</span><span class="w"> </span><span class="n">conn</span><span class="w"> </span><span class="o">-&gt;</span><span class="w">
          </span><span class="n">unsubscribe</span><span class="p" data-group-id="4486477585-13">(</span><span class="n">endpoint</span><span class="p">,</span><span class="w"> </span><span class="n">server_ref</span><span class="p" data-group-id="4486477585-13">)</span><span class="w">
          </span><span class="n">conn</span><span class="w">
        </span><span class="k" data-group-id="4486477585-12">end</span><span class="p" data-group-id="4486477585-11">)</span><span class="w">

      </span><span class="n">ref</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">make_ref</span><span class="p" data-group-id="4486477585-14">(</span><span class="p" data-group-id="4486477585-14">)</span><span class="w">
      </span><span class="ss">:ok</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">subscribe</span><span class="p" data-group-id="4486477585-15">(</span><span class="n">endpoint</span><span class="p">,</span><span class="w"> </span><span class="n">server_ref</span><span class="p" data-group-id="4486477585-15">)</span><span class="w">
      </span><span class="n">broadcast_from!</span><span class="p" data-group-id="4486477585-16">(</span><span class="n">endpoint</span><span class="p">,</span><span class="w"> </span><span class="n">server_ref</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="4486477585-17">{</span><span class="ss">:subscribe</span><span class="p">,</span><span class="w"> </span><span class="n">client_ref</span><span class="p" data-group-id="4486477585-18">(</span><span class="n">server_ref</span><span class="p" data-group-id="4486477585-18">)</span><span class="p">,</span><span class="w"> </span><span class="n">ref</span><span class="p" data-group-id="4486477585-17">}</span><span class="p" data-group-id="4486477585-16">)</span><span class="w">

      </span><span class="k">receive</span><span class="w"> </span><span class="k" data-group-id="4486477585-19">do</span><span class="w">
        </span><span class="p" data-group-id="4486477585-20">{</span><span class="ss">:subscribe</span><span class="p">,</span><span class="w"> </span><span class="o">^</span><span class="n">ref</span><span class="p" data-group-id="4486477585-20">}</span><span class="w"> </span><span class="o">-&gt;</span><span class="w"> </span><span class="p" data-group-id="4486477585-21">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="n">new_conn</span><span class="p">,</span><span class="w"> </span><span class="n">server_ref</span><span class="p" data-group-id="4486477585-21">}</span><span class="w">
      </span><span class="k" data-group-id="4486477585-19">after</span><span class="w">
        </span><span class="n">opts</span><span class="p" data-group-id="4486477585-22">[</span><span class="ss">:pubsub_timeout_ms</span><span class="p" data-group-id="4486477585-22">]</span><span class="w"> </span><span class="o">-&gt;</span><span class="w"> </span><span class="ss">:error</span><span class="w">
      </span><span class="k" data-group-id="4486477585-19">end</span><span class="w">

    </span><span class="bp">_</span><span class="w"> </span><span class="o">-&gt;</span><span class="w">
      </span><span class="ss">:error</span><span class="w">
  </span><span class="k" data-group-id="4486477585-6">end</span><span class="w">
</span><span class="k" data-group-id="4486477585-4">end</span></code></pre>
<p>
That’s it!</p>
<p>
The difference between WebSocket and LongPoll transports is that LongPoll requires all nodes to be connected, since to resume a session it needs to broadcast from the PID that the session was initiated on.</p>
<p>
The difference between staging and production is that staging had just one server running, while production had two. The AWS load balancer was consistently flipping between the two servers for each long poll request.</p>
<p>
So the solution is to enable distribution for LongPoll fallback to work.</p>
<h2>
Conclusion</h2>
<p>
While I’m not sure of the exact reasons why LongPoll requires distribution, I guess it is because WebSocket’s persistent connection makes it very simple to keep process state, while LongPoll has to tie multiple isolated REST requests together to achieve the same process state. In any case, you must enable distribution for your Phoenix app to function correctly with PubSub messages across all nodes.</p>
<p>
I hope this post can save some time for others in a similar situation.</p>

      ]]>
    </content>
  </entry>
  
  <entry>
    <title>Development helpers in Elixir</title>
    <link href="https://danschultzer.com/posts/development-helpers-in-elixir" />
    <id>https://danschultzer.com/posts/development-helpers-in-elixir</id>
    <updated>2024-03-24T00:00:00Z</updated>
    <summary>How I set up development helpers in my Elixir projects.</summary>
    <content type="html">
      <![CDATA[
        <p>
How do you efficiently set up development-only logic in Elixir? In one directory: <code class="inline">_dev</code>.</p>
<p>
This directory will contain all your development-only logic. Combined with <code class="inline">config/dev.exs</code> configuration, this makes a clear and safe separation from your production logic. There will be no need for awkward <code class="inline">Mix.env() == :dev</code> blocks around inline code (apart from two Phoenix files). The following example will be based on a Phoenix project, but you can use this pattern for any Elixir project.</p>
<p>
Let us get started!</p>
<h2>
<code class="inline">mix.exs</code> and <code class="inline">.formatter.exs</code></h2>
<p>
First, we want to include <code class="inline">_dev</code> files in our <code class="inline">dev</code> and <code class="inline">test</code> environments by updating <code class="inline">elixirc_paths</code> in <code class="inline">mix.exs</code>:</p>
<pre><code class="makeup elixir"><span class="kd">defmodule</span><span class="w"> </span><span class="nc">MyApp.MixProject</span><span class="w"> </span><span class="k" data-group-id="7216634634-1">do</span><span class="w">
  </span><span class="kn">use</span><span class="w"> </span><span class="nc">Mix.Project</span><span class="w">

  </span><span class="c1"># ...</span><span class="w">

  </span><span class="c1"># Specifies which paths to compile per environment.</span><span class="w">
  </span><span class="c1"># We include `_dev` directory in the test/dev env to</span><span class="w">
  </span><span class="c1"># enable the dev tools</span><span class="w">
  </span><span class="kd">defp</span><span class="w"> </span><span class="nf">elixirc_paths</span><span class="p" data-group-id="7216634634-2">(</span><span class="ss">:test</span><span class="p" data-group-id="7216634634-2">)</span><span class="p">,</span><span class="w"> </span><span class="ss">do</span><span class="p">:</span><span class="w"> </span><span class="p" data-group-id="7216634634-3">[</span><span class="s">&quot;lib&quot;</span><span class="p">,</span><span class="w"> </span><span class="s">&quot;test/support&quot;</span><span class="p">,</span><span class="w"> </span><span class="s">&quot;_dev&quot;</span><span class="p">,</span><span class="w"> </span><span class="s">&quot;test/_dev/support&quot;</span><span class="p" data-group-id="7216634634-3">]</span><span class="w">
  </span><span class="kd">defp</span><span class="w"> </span><span class="nf">elixirc_paths</span><span class="p" data-group-id="7216634634-4">(</span><span class="ss">:dev</span><span class="p" data-group-id="7216634634-4">)</span><span class="p">,</span><span class="w"> </span><span class="ss">do</span><span class="p">:</span><span class="w"> </span><span class="p" data-group-id="7216634634-5">[</span><span class="s">&quot;lib&quot;</span><span class="p">,</span><span class="w"> </span><span class="s">&quot;_dev&quot;</span><span class="p" data-group-id="7216634634-5">]</span><span class="w">
  </span><span class="kd">defp</span><span class="w"> </span><span class="nf">elixirc_paths</span><span class="p" data-group-id="7216634634-6">(</span><span class="bp">_</span><span class="p" data-group-id="7216634634-6">)</span><span class="p">,</span><span class="w"> </span><span class="ss">do</span><span class="p">:</span><span class="w"> </span><span class="p" data-group-id="7216634634-7">[</span><span class="s">&quot;lib&quot;</span><span class="p" data-group-id="7216634634-7">]</span><span class="w">

  </span><span class="c1"># ...</span><span class="w">
</span><span class="k" data-group-id="7216634634-1">end</span></code></pre>
<p>
We also want <code class="inline">mix format</code> to pick up on it, so update the <code class="inline">.formatter.exs</code>:</p>
<pre><code class="makeup elixir"><span class="p" data-group-id="1970789082-1">[</span><span class="w">
  </span><span class="ss">import_deps</span><span class="p">:</span><span class="w"> </span><span class="p" data-group-id="1970789082-2">[</span><span class="ss">:phoenix</span><span class="p" data-group-id="1970789082-2">]</span><span class="p">,</span><span class="w">
  </span><span class="ss">plugins</span><span class="p">:</span><span class="w"> </span><span class="p" data-group-id="1970789082-3">[</span><span class="nc">Phoenix.LiveView.HTMLFormatter</span><span class="p" data-group-id="1970789082-3">]</span><span class="p">,</span><span class="w">
  </span><span class="ss">inputs</span><span class="p">:</span><span class="w"> </span><span class="p" data-group-id="1970789082-4">[</span><span class="s">&quot;*.{heex,ex,exs}&quot;</span><span class="p">,</span><span class="w"> </span><span class="s">&quot;{_dev,config,lib,test}/**/*.{heex,ex,exs}&quot;</span><span class="p" data-group-id="1970789082-4">]</span><span class="w">
</span><span class="p" data-group-id="1970789082-1">]</span></code></pre>
<p>
That is it. Now you can put all your dev-only files in the <code class="inline">_dev</code> directory and it will only ever get included in the <code class="inline">dev</code> and <code class="inline">test</code> environments. We can test our dev-only logic and add dev-only test support files to <code class="inline">test/_dev/support</code>.</p>
<h2>
Example: Seed tool</h2>
<p>
Let us set up a development helper to manually seed a variety of scenarios in our environment.</p>
<p>
First, we may want to add some helpers in <code class="inline">_dev/my_app</code>, for example <code class="inline">_dev/my_app/seed_helper.exs</code>:</p>
<pre><code class="makeup elixir"><span class="kd">defmodule</span><span class="w"> </span><span class="nc">MyApp.Dev.SeedHelper</span><span class="w"> </span><span class="k" data-group-id="7038372157-1">do</span><span class="w">
  </span><span class="c1"># ...</span><span class="w">
</span><span class="k" data-group-id="7038372157-1">end</span></code></pre>
<p>
I’m namespacing the module names with <code class="inline">Dev</code>. Though it’s just a dev-only tool we still need to test it, so we’ll create the test file in <code class="inline">test/_dev/my_app/seed_helper_test.exs</code>:</p>
<pre><code class="makeup elixir"><span class="kd">defmodule</span><span class="w"> </span><span class="nc">MyApp.Dev.SeedHelperTest</span><span class="w"> </span><span class="k" data-group-id="9919054683-1">do</span><span class="w">
  </span><span class="kn">use</span><span class="w"> </span><span class="nc">MyApp.DataCase</span><span class="w">

  </span><span class="c1"># ...</span><span class="w">
</span><span class="k" data-group-id="9919054683-1">end</span></code></pre>
<p>
For Phoenix, we’ll have to update our router to enable our dev-only routes. This along with navigation in the UI will be the only parts that can’t be completely separated from anything that touches production code.</p>
<pre><code class="makeup elixir"><span class="kd">defmodule</span><span class="w"> </span><span class="nc">MyAppWeb.Router</span><span class="w"> </span><span class="k" data-group-id="8804891673-1">do</span><span class="w">
  </span><span class="kn">use</span><span class="w"> </span><span class="nc">MyAppWeb</span><span class="p">,</span><span class="w"> </span><span class="ss">:router</span><span class="w">

  </span><span class="c1"># ...</span><span class="w">

  </span><span class="c1"># Enable dev tools for dev and test env</span><span class="w">
  </span><span class="k">if</span><span class="w"> </span><span class="nc">Application</span><span class="o">.</span><span class="n">compile_env</span><span class="p" data-group-id="8804891673-2">(</span><span class="ss">:my_app_web</span><span class="p">,</span><span class="w"> </span><span class="ss">:dev_tools</span><span class="p" data-group-id="8804891673-2">)</span><span class="w"> </span><span class="k" data-group-id="8804891673-3">do</span><span class="w">
    </span><span class="n">scope</span><span class="w"> </span><span class="s">&quot;/dev&quot;</span><span class="w"> </span><span class="k" data-group-id="8804891673-4">do</span><span class="w">
      </span><span class="n">pipe_through</span><span class="w"> </span><span class="ss">:browser</span><span class="w">

      </span><span class="n">scope</span><span class="w"> </span><span class="s">&quot;/tools&quot;</span><span class="p">,</span><span class="w"> </span><span class="nc">MyAppWeb.Dev.Tools</span><span class="w"> </span><span class="k" data-group-id="8804891673-5">do</span><span class="w">
        </span><span class="n">scope</span><span class="w"> </span><span class="s">&quot;/seeds&quot;</span><span class="w"> </span><span class="k" data-group-id="8804891673-6">do</span><span class="w">
          </span><span class="n">live</span><span class="w"> </span><span class="s">&quot;/&quot;</span><span class="p">,</span><span class="w"> </span><span class="nc">SeedLive</span><span class="p">,</span><span class="w"> </span><span class="ss">:index</span><span class="w">
          </span><span class="n">live</span><span class="w"> </span><span class="s">&quot;/:id/run&quot;</span><span class="p">,</span><span class="w"> </span><span class="nc">SeedLive</span><span class="p">,</span><span class="w"> </span><span class="ss">:run</span><span class="w">
        </span><span class="k" data-group-id="8804891673-6">end</span><span class="w">
      </span><span class="k" data-group-id="8804891673-5">end</span><span class="w">
    </span><span class="k" data-group-id="8804891673-4">end</span><span class="w">
  </span><span class="k" data-group-id="8804891673-3">end</span><span class="w">
</span><span class="k" data-group-id="8804891673-1">end</span></code></pre>
<p>
In this case, I set a config to enable the dev tools in <code class="inline">config/config.exs</code> as:</p>
<pre><code class="makeup elixir"><span class="n">config</span><span class="w"> </span><span class="ss">:my_app_web</span><span class="p">,</span><span class="w"> </span><span class="ss">:dev_tools</span><span class="p">,</span><span class="w"> </span><span class="nc">Mix</span><span class="o">.</span><span class="n">env</span><span class="p" data-group-id="3819238529-1">(</span><span class="p" data-group-id="3819238529-1">)</span><span class="w"> </span><span class="ow">in</span><span class="w"> </span><span class="p" data-group-id="3819238529-2">[</span><span class="ss">:dev</span><span class="p">,</span><span class="w"> </span><span class="ss">:test</span><span class="p" data-group-id="3819238529-2">]</span></code></pre>
<p>
You could also just use <code class="inline">Mix.env()</code> straight in the router, but I found the config pattern more comfortable.</p>
<p>
We should add a link in our template as well:</p>
<pre><code class="makeup elixir"><span class="o">&lt;</span><span class="p">%</span><span class="o">=</span><span class="w"> </span><span class="k">if</span><span class="w"> </span><span class="nc">Application</span><span class="o">.</span><span class="n">get_env</span><span class="p" data-group-id="8187018888-1">(</span><span class="ss">:my_app_web</span><span class="p">,</span><span class="w"> </span><span class="ss">:dev_tools</span><span class="p" data-group-id="8187018888-1">)</span><span class="w"> </span><span class="k" data-group-id="8187018888-2">do</span><span class="w"> </span><span class="p">%</span><span class="o">&gt;</span><span class="w">
  </span><span class="o">&lt;</span><span class="o">.</span><span class="n">link</span><span class="w"> </span><span class="n">navigate</span><span class="o">=</span><span class="p" data-group-id="8187018888-3">{</span><span class="s">&quot;/dev/tools/seed&quot;</span><span class="p" data-group-id="8187018888-3">}</span><span class="o">&gt;</span><span class="nc">Seed</span><span class="o">&lt;</span><span class="o">/</span><span class="o">.</span><span class="n">link</span><span class="o">&gt;</span><span class="w">
</span><span class="o">&lt;</span><span class="p">%</span><span class="w"> </span><span class="k" data-group-id="8187018888-2">end</span><span class="w"> </span><span class="p">%</span><span class="o">&gt;</span></code></pre>
<p>
Now we’ll set up our dev tools, it could be in <code class="inline">_dev/my_app_web/tools/live/seed_live.ex</code>:</p>
<pre><code class="makeup elixir"><span class="kd">defmodule</span><span class="w"> </span><span class="nc">MyAppWeb.Dev.Tools.SeedLive</span><span class="w"> </span><span class="k" data-group-id="9761135562-1">do</span><span class="w">
  </span><span class="kn">use</span><span class="w"> </span><span class="nc">MyAppWeb</span><span class="p">,</span><span class="w"> </span><span class="ss">:live_view</span><span class="w">

  </span><span class="na">@impl</span><span class="w"> </span><span class="no">true</span><span class="w">
  </span><span class="kd">def</span><span class="w"> </span><span class="nf">mount</span><span class="p" data-group-id="9761135562-2">(</span><span class="c">_params</span><span class="p">,</span><span class="w"> </span><span class="c">_session</span><span class="p">,</span><span class="w"> </span><span class="n">socket</span><span class="p" data-group-id="9761135562-2">)</span><span class="w"> </span><span class="k" data-group-id="9761135562-3">do</span><span class="w">
    </span><span class="p" data-group-id="9761135562-4">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="n">stream</span><span class="p" data-group-id="9761135562-5">(</span><span class="n">socket</span><span class="p">,</span><span class="w"> </span><span class="ss">:seeds</span><span class="p">,</span><span class="w"> </span><span class="n">list_seed_files</span><span class="p" data-group-id="9761135562-6">(</span><span class="p" data-group-id="9761135562-6">)</span><span class="p" data-group-id="9761135562-5">)</span><span class="p" data-group-id="9761135562-4">}</span><span class="w">
  </span><span class="k" data-group-id="9761135562-3">end</span><span class="w">

  </span><span class="c1"># ...</span><span class="w">
</span><span class="k" data-group-id="9761135562-1">end</span></code></pre>
<p>
A test for the above should be in <code class="inline">test/_dev/my_app_web/tools/seed_live_test.exs</code>.</p>
<pre><code class="makeup elixir"><span class="kd">defmodule</span><span class="w"> </span><span class="nc">MyAppWeb.Dev.Tools.SeedLiveTest</span><span class="w"> </span><span class="k" data-group-id="6920236597-1">do</span><span class="w">
  </span><span class="kn">use</span><span class="w"> </span><span class="nc">MyAppWeb.ConnCase</span><span class="w">

  </span><span class="n">test</span><span class="w"> </span><span class="s">&quot;lists and run seeds&quot;</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="6920236597-2">%{</span><span class="ss">conn</span><span class="p">:</span><span class="w"> </span><span class="n">conn</span><span class="p" data-group-id="6920236597-2">}</span><span class="w"> </span><span class="k" data-group-id="6920236597-3">do</span><span class="w">
    </span><span class="c1"># ...</span><span class="w">
  </span><span class="k" data-group-id="6920236597-3">end</span><span class="w">
</span><span class="k" data-group-id="6920236597-1">end</span></code></pre>
<p>
This has worked very well for my team, and I highly recommend this pattern.</p>

      ]]>
    </content>
  </entry>
  
  <entry>
    <title>JavaScript source maps with Phoenix 1.7</title>
    <link href="https://danschultzer.com/posts/javascript-source-maps-with-phoenix-1-7" />
    <id>https://danschultzer.com/posts/javascript-source-maps-with-phoenix-1-7</id>
    <updated>2024-03-13T00:00:00Z</updated>
    <summary>How to generate and upload JavaScript source maps with Phoenix 1.7 to Sentry with GitHub Actions.</summary>
    <content type="html">
      <![CDATA[
        <p>
If you are setting up error tracking for the JavaScript assets in your Phoenix app, you’ll need a <a href="https://firefox-source-docs.mozilla.org/devtools-user/debugger/how_to/use_a_source_map/index.html#">source map</a> to enable readable stack traces. In the following, I’m setting this up with <a href="https://sentry.io">Sentry</a> for error tracking and GitHub Actions for CI/CD, but you can easily adapt this to any vendor.</p>
<h2>
Generating the source maps</h2>
<p>
With Phoenix 1.7 you generate the source maps by setting the <code class="inline">--sourcemap</code> flag for <code class="inline">esbuild</code> in <code class="inline">config/config.exs</code>:</p>
<pre><code class="makeup elixir"><span class="n">config</span><span class="w"> </span><span class="ss">:esbuild</span><span class="p">,</span><span class="w">
  </span><span class="ss">version</span><span class="p">:</span><span class="w"> </span><span class="s">&quot;0.17.11&quot;</span><span class="p">,</span><span class="w">
  </span><span class="ss">my_app_web</span><span class="p">:</span><span class="w"> </span><span class="p" data-group-id="6064775210-1">[</span><span class="w">
    </span><span class="ss">args</span><span class="p">:</span><span class="w">
      </span><span class="sx">~w(js/app.js --bundle --target=es2017 --outdir=../priv/static/assets --external:/fonts/* --external:/images/* --sourcemap=external)</span><span class="p">,</span><span class="w">
    </span><span class="ss">cd</span><span class="p">:</span><span class="w"> </span><span class="nc">Path</span><span class="o">.</span><span class="n">expand</span><span class="p" data-group-id="6064775210-2">(</span><span class="s">&quot;../assets&quot;</span><span class="p">,</span><span class="w"> </span><span class="bp">__DIR__</span><span class="p" data-group-id="6064775210-2">)</span><span class="p">,</span><span class="w">
    </span><span class="ss">env</span><span class="p">:</span><span class="w"> </span><span class="p" data-group-id="6064775210-3">%{</span><span class="s">&quot;NODE_PATH&quot;</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="nc">Path</span><span class="o">.</span><span class="n">expand</span><span class="p" data-group-id="6064775210-4">(</span><span class="s">&quot;../deps&quot;</span><span class="p">,</span><span class="w"> </span><span class="bp">__DIR__</span><span class="p" data-group-id="6064775210-4">)</span><span class="p" data-group-id="6064775210-3">}</span><span class="w">
  </span><span class="p" data-group-id="6064775210-1">]</span></code></pre>
<p>
In the above, I use <code class="inline">--sourcemap=external</code> flag to generate the source map but importantly not include the reference in our <code class="inline">.js</code> file. If you are fine with exposing the source map to the public you can just use <code class="inline">--sourcemap</code> and be done with it; the browser can show the full stack trace and your error tracker might be able to automatically pick it up.</p>
<p>
In my case, I want to keep the source map to myself, so I have to move it out of the assets directory in my build stage so it will not be included in my release build.</p>
<p>
Add the following commands in the <code class="inline">Dockerfile</code> right after the assets are compiled with <code class="inline">mix assets.deploy</code>:</p>
<pre><code class="Dockerfile"># ...

# Compile assets
RUN mix assets.deploy

# Move source maps out of assets
RUN mkdir ./sourcemaps
RUN mv apps/my_app_web/priv/static/assets/*.js.map ./sourcemaps
# ...</code></pre>
<p>
With these commands I’m moving the source map files out of <code class="inline">priv/static/assets</code> to a place that will not be included in my release.</p>
<h2>
Upload the source maps</h2>
<p>
I’m using Github Actions for my CI/CD and will be using the <a href="https://github.com/marketplace/actions/sentry-release">Sentry Release Github Action</a> task to upload the source map. This means that I first need to extract the source maps from the build.</p>
<p>
Here we add a new stage named <code class="inline">sourcemaps</code> to our <code class="inline">Dockerfile</code> just before our final release stage:</p>
<pre><code class="Dockerfile"># ...
# sourcemaps is a target used as remote context to push source maps to sentry
FROM scratch AS sourcemaps
COPY --from=builder /app/sourcemaps/ /

# Start a new build stage so that the final image will only contain
# the compiled release and other runtime necessities
FROM ${RUNNER_IMAGE}
# ...</code></pre>
<p>
The only thing we do in this stage is to copy the generated source maps so we can pull them out of the build to the host. We can pull them out of the build by running <code class="inline">docker build . --output ./sourcemaps --target sourcemaps</code>.</p>
<p>
Now we can set up Github Actions to upload the source maps when we build the release:</p>
<pre><code class="yaml"># ...
- name: Build and push image
  id: docker-build
  uses: docker/build-push-action@v4
  with:
    cache-from: type=gha
    cache-to: type=gha,mode=max
    # push: true
    # tags: ...
    # ...

- name: Output source maps
  uses: docker/build-push-action@v4
  with:
    target: sourcemaps
    cache-from: type=gha
    cache-to: type=gha,mode=max
    outputs: type=local,dest=./sourcemaps

- name: Create Sentry release
  uses: getsentry/action-release@v1
  env:
    SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
    SENTRY_ORG: ${{ vars.SENTRY_ORG }}
    SENTRY_PROJECT: ${{ vars.SENTRY_PROJECT }}
  with:
    version: ${{ github.sha }}
    sourcemaps: ./sourcemaps</code></pre>
<p>
We are using the <a href="https://github.com/marketplace/actions/build-and-push-docker-images">Build and Push docker images</a> GHA task to first build and push our release image. After that, we output the source maps to the <code class="inline">./sourcemaps</code> directory on our GHA host. Now the Sentry Release Github Actions task can pick them up and upload them.</p>
<p>
Note that we are caching the layers so we won’t rebuild the image when running the ouput source maps task after the build and push.</p>

      ]]>
    </content>
  </entry>
  
  <entry>
    <title>Content Security Policy header with Phoenix LiveView</title>
    <link href="https://danschultzer.com/posts/content-security-policy-with-liveview" />
    <id>https://danschultzer.com/posts/content-security-policy-with-liveview</id>
    <updated>2024-03-07T00:00:00Z</updated>
    <summary>How to set up Content Security Policy with Phoenix LiveView.</summary>
    <content type="html">
      <![CDATA[
        <p>
If you use <a href="https://hexdocs.pm/sobelow/readme.html"><code class="inline">sobelow</code></a> you will get a <code class="inline">Missing Content-Security-Policy</code> error with a default Phoenix setup. Managing <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP">Content Security Policy</a> (CSP) can be tedious, so you may just want to opt for a catch-all <code class="inline">&#39;unsafe-inline&#39;</code> approach to bypass it all. We should do better than that.</p>
<p>
In this blog post, I’ll build a module with helpers to integrate CSP handling into Phoenix and Phoenix LiveView. I think the code is too minimal to be a dependency, but some of it could maybe be added upstream in the <code class="inline">put_secure_browser_headers</code> plug in Phoenix.</p>
<h3>
Writing our first CSP header</h3>
<p>
CSP headers consist of directives with sources. The recommendation for a default policy is <code class="inline">default-src &#39;self&#39;;</code>. This mitigates most typical cross-site scripting (XSS) attacks. <code class="inline">&#39;self&#39;</code> means that only resources of the same scheme, host, and port, as the page itself can be executed.</p>
<p>
With Phoenix we have a bunch of <code class="inline">data:</code> blobs from the hero icons. These can’t be loaded with the above policy so we should add <code class="inline">img-src: &#39;self&#39; data:;</code> to our policy. To add a second source, like a CDN, we just add it to the sources list: <code class="inline">default-src &#39;self&#39; https://cdn.example.com;</code>.</p>
<p>
This is straightforward, but now comes the tricky part. What if we have inline styles or scripts?</p>
<p>
If we can’t move it out of the DOM we should use <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/Sources#hash-algorithm-base64-value">CSP hashing</a>. The tag will only be executed if the hashed value of the content matches the CSP source hash. As an example, we can set up the sources list with <code class="inline">script-src &#39;sha256-RFWPLDbv2BY+rCkDzsE+0fr8ylGr2R2faWMhq4lfEQc=&#39;;</code> for the following inline script to execute:</p>
<pre><code class="html">&lt;script&gt;doSomething();&lt;/script&gt;</code></pre>
<p>
What if we have styles or scripts with dynamic content? If we are only dealing with REST this would be trivial as we can just produce the hash on each request, but we got our LiveView socket pushing diffs while the CSP header has already been sent. If possible we could calculate all the possible hash values and add them to the CSP source list, but in my case, there were way too many variants.</p>
<p>
We shouldn’t opt for the obviously unsafe <code class="inline">&#39;unsafe-inline&#39;</code>. Instead, we will use a <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/nonce">nonce</a>.</p>
<p>
On the initial request, we will generate the nonce and then ensure we use the same nonce throughout the LiveView session.</p>
<h3>
An inline style example</h3>
<p>
To use inline styles with nonces we must use <code class="inline">&lt;style&gt;</code> tags. Elements with style attributes only support CSP hashes. So we must move any <code class="inline">style</code> attributes out to <code class="inline">&lt;style&gt;</code> tags with a <code class="inline">nonce</code> attribute:</p>
<pre><code class="makeup elixir"><span class="o">&lt;</span><span class="n">span</span><span class="w"> </span><span class="n">id</span><span class="o">=</span><span class="s">&quot;progress&quot;</span><span class="o">&gt;</span><span class="o">&lt;</span><span class="o">/</span><span class="n">span</span><span class="o">&gt;</span><span class="w">

</span><span class="o">&lt;</span><span class="n">style</span><span class="w"> </span><span class="n">nonce</span><span class="o">=</span><span class="p" data-group-id="9633025242-1">{</span><span class="n">get_csp_nonce</span><span class="p" data-group-id="9633025242-2">(</span><span class="p" data-group-id="9633025242-2">)</span><span class="p" data-group-id="9633025242-1">}</span><span class="o">&gt;</span><span class="w">
  </span><span class="c1">#progress {</span><span class="w">
    </span><span class="ss">width</span><span class="p">:</span><span class="w"> </span><span class="o">&lt;</span><span class="p">%</span><span class="o">=</span><span class="w"> </span><span class="na">@progress</span><span class="w"> </span><span class="p">%</span><span class="o">&gt;</span><span class="p">%</span><span class="p">;</span><span class="w">
  </span><span class="err">}</span><span class="w">
</span><span class="o">&lt;</span><span class="o">/</span><span class="n">style</span><span class="o">&gt;</span></code></pre>
<h3>
CSP helpers</h3>
<p>
Now let us set up our CSP module. It will look similar to the CSRF plug. The important points here are:</p>
<ul>
  <li>
A unique nonce is used on each REST response  </li>
  <li>
The nonce is generated using a cryptographically secure random number generator  </li>
  <li>
The nonce has at least 128 bits of entropy  </li>
  <li>
The same nonce is used across the initial request process  </li>
  <li>
The same nonce is used across the LiveView Socket  </li>
</ul>
<pre><code class="makeup elixir"><span class="kd">defmodule</span><span class="w"> </span><span class="nc">MyAppWeb.CSP</span><span class="w"> </span><span class="k" data-group-id="8464535524-1">do</span><span class="w">
  </span><span class="na">@moduledoc</span><span class="w"> </span><span class="s">&quot;&quot;&quot;
  Module that includes Plug and LiveView helpers to handle Content Security
  Policy header.

  For inline `&lt;style&gt;` and `&lt;script&gt;` tags, nonce should be used. When the REST
  request is processed a nonce is added to the process dictionary. This ensures
  the nonce stays the same throughout the call, as the nonce in the tags must
  match the nonce in the header.

  To allow for inline `&lt;style&gt;` and/or `&lt;script&gt;` tag you must set a `&#39;nonce&#39;`
  source.

  ## Set up

  To set up MyAppWeb.CSP in your app you must:

  ### 1) Configure `lib/my_app_web.ex`

  Ensure you import the helpers in `MyAppWeb`.

    def router do
      quote do
        use Phoenix.Router, helpers: false

        # Import common connection and controller functions to use in pipelines
        import Plug.Conn
        import Phoenix.Controller
        import Phoenix.LiveView.Router

        import MyAppWeb.CSP, only: [put_content_security_policy: 2]
      end
    end

    # ...

    def html do
      quote do
        use Phoenix.Component

        import MyAppWeb.CldrHelpers

        # Import convenience functions from controllers
        import Phoenix.Controller,
          only: [get_csrf_token: 0, view_module: 1, view_template: 1]

        import MyAppWeb.CSP,
          only: [get_csp_nonce: 0]

        # Include general helpers for rendering HTML
        unquote(html_helpers())
      end
    end

    # ...

    def live_view do
      quote do
        use Phoenix.LiveView,
          layout: {MyAppWeb.Layouts, :app}

        on_mount MyAppWeb.CSP

        unquote(html_helpers())
      end
    end

  ### 2) Add nonce metatag to the HTML document

  Add the following metatag head to
  `lib/my_app_web/components/layouts/root.html.heex`.

    &lt;meta name=&quot;csp-nonce&quot; content={get_csp_nonce()} /&gt;

  ### 3) Pass the CSP nonce to the LiveView socket

  Ensure you pass on the CSP nonce to the LiveView socket in
  `assets/js/app.js`.

    let csrfToken = document.querySelector(&quot;meta[name=&#39;csrf-token&#39;]&quot;).getAttribute(&quot;content&quot;);
    let cspNonce = document.querySelector(&quot;meta[name=&#39;csp-nonce&#39;]&quot;).getAttribute(&quot;content&quot;)
    let liveSocket = new LiveSocket(&quot;/live&quot;, Socket, {
      longPollFallbackMs: 2500,
      params: { _csrf_token: csrfToken, _csp_nonce: cspNonce }
    })

  ## Usage

  If you got inline `&lt;style&gt;` or script tags you must set the nonce attribute:

      &lt;style nonce={get_csp_nonce()}&gt;
        // ...
      &lt;/style&gt;
  &quot;&quot;&quot;</span><span class="w">
  </span><span class="kn">require</span><span class="w"> </span><span class="nc">Logger</span><span class="w">

  </span><span class="kn">import</span><span class="w"> </span><span class="nc">Plug.Conn</span><span class="w">

  </span><span class="na">@doc</span><span class="w"> </span><span class="s">&quot;&quot;&quot;
  Sets a content security policy header.

  By default the policy is `default-src &#39;self&#39;`. `&#39;nonce&#39;` source will be
  expanded with an auto-generated nonce that is persisted in the process
  dictionary.

  The options can be a function or a keyword list. Sources can be a binary
  or list of binaries. Duplicate directives will be merged together.

  ## Example

    plug :put_content_security_policy,
      img_src: &quot;&#39;self&#39; data:`,
      style_src: &quot;&#39;self&#39; &#39;nonce&#39;&quot;

    plug :put_content_security_policy,
      img_src: [
        &quot;&#39;self&#39;&quot;,
        &quot;data:&quot;
      ]

    plug :put_content_security_policy, &amp;MyAppWeb.CSPPolicy.opts/1
  &quot;&quot;&quot;</span><span class="w">
  </span><span class="kd">def</span><span class="w"> </span><span class="nf">put_content_security_policy</span><span class="p" data-group-id="8464535524-2">(</span><span class="n">conn</span><span class="p">,</span><span class="w"> </span><span class="n">fun</span><span class="p" data-group-id="8464535524-2">)</span><span class="w"> </span><span class="ow">when</span><span class="w"> </span><span class="n">is_function</span><span class="p" data-group-id="8464535524-3">(</span><span class="n">fun</span><span class="p">,</span><span class="w"> </span><span class="mi">1</span><span class="p" data-group-id="8464535524-3">)</span><span class="w"> </span><span class="k" data-group-id="8464535524-4">do</span><span class="w">
    </span><span class="n">put_content_security_policy</span><span class="p" data-group-id="8464535524-5">(</span><span class="n">conn</span><span class="p">,</span><span class="w"> </span><span class="n">fun</span><span class="o">.</span><span class="p" data-group-id="8464535524-6">(</span><span class="n">conn</span><span class="p" data-group-id="8464535524-6">)</span><span class="p" data-group-id="8464535524-5">)</span><span class="w">
  </span><span class="k" data-group-id="8464535524-4">end</span><span class="w">

  </span><span class="kd">def</span><span class="w"> </span><span class="nf">put_content_security_policy</span><span class="p" data-group-id="8464535524-7">(</span><span class="n">conn</span><span class="p">,</span><span class="w"> </span><span class="n">opts</span><span class="p" data-group-id="8464535524-7">)</span><span class="w"> </span><span class="ow">when</span><span class="w"> </span><span class="n">is_list</span><span class="p" data-group-id="8464535524-8">(</span><span class="n">opts</span><span class="p" data-group-id="8464535524-8">)</span><span class="w"> </span><span class="k" data-group-id="8464535524-9">do</span><span class="w">
    </span><span class="n">csp</span><span class="w"> </span><span class="o">=</span><span class="w">
      </span><span class="n">opts</span><span class="w">
      </span><span class="o">|&gt;</span><span class="w"> </span><span class="nc">Keyword</span><span class="o">.</span><span class="n">has_key?</span><span class="p" data-group-id="8464535524-10">(</span><span class="ss">:default_src</span><span class="p" data-group-id="8464535524-10">)</span><span class="w">
      </span><span class="o">|&gt;</span><span class="w"> </span><span class="k">case</span><span class="w"> </span><span class="k" data-group-id="8464535524-11">do</span><span class="w">
        </span><span class="no">false</span><span class="w"> </span><span class="o">-&gt;</span><span class="w"> </span><span class="p" data-group-id="8464535524-12">[</span><span class="ss">default_src</span><span class="p">:</span><span class="w"> </span><span class="s">&quot;&#39;self&#39;&quot;</span><span class="p" data-group-id="8464535524-12">]</span><span class="w"> </span><span class="o">++</span><span class="w"> </span><span class="n">opts</span><span class="w">
        </span><span class="no">true</span><span class="w"> </span><span class="o">-&gt;</span><span class="w"> </span><span class="n">opts</span><span class="w">
      </span><span class="k" data-group-id="8464535524-11">end</span><span class="w">
      </span><span class="o">|&gt;</span><span class="w"> </span><span class="nc">Enum</span><span class="o">.</span><span class="n">reduce</span><span class="p" data-group-id="8464535524-13">(</span><span class="p" data-group-id="8464535524-14">[</span><span class="p" data-group-id="8464535524-14">]</span><span class="p">,</span><span class="w"> </span><span class="k" data-group-id="8464535524-15">fn</span><span class="p" data-group-id="8464535524-16">{</span><span class="n">name</span><span class="p">,</span><span class="w"> </span><span class="n">sources</span><span class="p" data-group-id="8464535524-16">}</span><span class="p">,</span><span class="w"> </span><span class="n">acc</span><span class="w"> </span><span class="o">-&gt;</span><span class="w">
        </span><span class="n">sources</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">List</span><span class="o">.</span><span class="n">wrap</span><span class="p" data-group-id="8464535524-17">(</span><span class="n">sources</span><span class="p" data-group-id="8464535524-17">)</span><span class="w">

        </span><span class="nc">Keyword</span><span class="o">.</span><span class="n">update</span><span class="p" data-group-id="8464535524-18">(</span><span class="n">acc</span><span class="p">,</span><span class="w"> </span><span class="n">name</span><span class="p">,</span><span class="w"> </span><span class="n">sources</span><span class="p">,</span><span class="w"> </span><span class="o">&amp;</span><span class="w"> </span><span class="ni">&amp;1</span><span class="w"> </span><span class="o">++</span><span class="w"> </span><span class="n">sources</span><span class="p" data-group-id="8464535524-18">)</span><span class="w">
      </span><span class="k" data-group-id="8464535524-15">end</span><span class="p" data-group-id="8464535524-13">)</span><span class="w">
      </span><span class="o">|&gt;</span><span class="w"> </span><span class="nc">Enum</span><span class="o">.</span><span class="n">reduce</span><span class="p" data-group-id="8464535524-19">(</span><span class="s">&quot;&quot;</span><span class="p">,</span><span class="w"> </span><span class="k" data-group-id="8464535524-20">fn</span><span class="w"> </span><span class="p" data-group-id="8464535524-21">{</span><span class="n">name</span><span class="p">,</span><span class="w"> </span><span class="n">sources</span><span class="p" data-group-id="8464535524-21">}</span><span class="p">,</span><span class="w"> </span><span class="n">acc</span><span class="w"> </span><span class="o">-&gt;</span><span class="w">
        </span><span class="n">name</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">String</span><span class="o">.</span><span class="n">replace</span><span class="p" data-group-id="8464535524-22">(</span><span class="n">to_string</span><span class="p" data-group-id="8464535524-23">(</span><span class="n">name</span><span class="p" data-group-id="8464535524-23">)</span><span class="p">,</span><span class="w"> </span><span class="s">&quot;_&quot;</span><span class="p">,</span><span class="w"> </span><span class="s">&quot;-&quot;</span><span class="p" data-group-id="8464535524-22">)</span><span class="w">

        </span><span class="n">sources</span><span class="w"> </span><span class="o">=</span><span class="w">
          </span><span class="n">sources</span><span class="w">
          </span><span class="o">|&gt;</span><span class="w"> </span><span class="nc">Enum</span><span class="o">.</span><span class="n">uniq</span><span class="p" data-group-id="8464535524-24">(</span><span class="p" data-group-id="8464535524-24">)</span><span class="w">
          </span><span class="o">|&gt;</span><span class="w"> </span><span class="nc">Enum</span><span class="o">.</span><span class="n">join</span><span class="p" data-group-id="8464535524-25">(</span><span class="s">&quot; &quot;</span><span class="p" data-group-id="8464535524-25">)</span><span class="w">
          </span><span class="o">|&gt;</span><span class="w"> </span><span class="nc">String</span><span class="o">.</span><span class="n">replace</span><span class="p" data-group-id="8464535524-26">(</span><span class="s">&quot;&#39;nonce&#39;&quot;</span><span class="p">,</span><span class="w"> </span><span class="s">&quot;&#39;nonce-</span><span class="si" data-group-id="8464535524-27">#{</span><span class="n">get_csp_nonce</span><span class="p" data-group-id="8464535524-28">(</span><span class="p" data-group-id="8464535524-28">)</span><span class="si" data-group-id="8464535524-27">}</span><span class="s">&#39;&quot;</span><span class="p" data-group-id="8464535524-26">)</span><span class="w">

        </span><span class="s">&quot;</span><span class="si" data-group-id="8464535524-29">#{</span><span class="n">acc</span><span class="si" data-group-id="8464535524-29">}</span><span class="si" data-group-id="8464535524-30">#{</span><span class="n">name</span><span class="si" data-group-id="8464535524-30">}</span><span class="s"> </span><span class="si" data-group-id="8464535524-31">#{</span><span class="n">sources</span><span class="si" data-group-id="8464535524-31">}</span><span class="s">;&quot;</span><span class="w">
      </span><span class="k" data-group-id="8464535524-20">end</span><span class="p" data-group-id="8464535524-19">)</span><span class="w">

    </span><span class="n">put_resp_header</span><span class="p" data-group-id="8464535524-32">(</span><span class="n">conn</span><span class="p">,</span><span class="w"> </span><span class="s">&quot;content-security-policy&quot;</span><span class="p">,</span><span class="w"> </span><span class="n">csp</span><span class="p" data-group-id="8464535524-32">)</span><span class="w">
  </span><span class="k" data-group-id="8464535524-9">end</span><span class="w">

  </span><span class="na">@doc</span><span class="w"> </span><span class="s">&quot;&quot;&quot;
  Gets the CSP nonce.

  Generates a nonce and stores it in the process dictionary if one does not exist.
  &quot;&quot;&quot;</span><span class="w">
  </span><span class="kd">def</span><span class="w"> </span><span class="nf">get_csp_nonce</span><span class="w"> </span><span class="k" data-group-id="8464535524-33">do</span><span class="w">
    </span><span class="k">if</span><span class="w"> </span><span class="n">nonce</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">Process</span><span class="o">.</span><span class="n">get</span><span class="p" data-group-id="8464535524-34">(</span><span class="ss">:plug_csp_nonce</span><span class="p" data-group-id="8464535524-34">)</span><span class="w"> </span><span class="k" data-group-id="8464535524-35">do</span><span class="w">
      </span><span class="n">nonce</span><span class="w">
    </span><span class="k" data-group-id="8464535524-35">else</span><span class="w">
      </span><span class="n">nonce</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">csp_nonce</span><span class="p" data-group-id="8464535524-36">(</span><span class="p" data-group-id="8464535524-36">)</span><span class="w">
      </span><span class="nc">Process</span><span class="o">.</span><span class="n">put</span><span class="p" data-group-id="8464535524-37">(</span><span class="ss">:plug_csp_nonce</span><span class="p">,</span><span class="w"> </span><span class="n">nonce</span><span class="p" data-group-id="8464535524-37">)</span><span class="w">
      </span><span class="n">nonce</span><span class="w">
    </span><span class="k" data-group-id="8464535524-35">end</span><span class="w">
  </span><span class="k" data-group-id="8464535524-33">end</span><span class="w">

  </span><span class="kd">defp</span><span class="w"> </span><span class="nf">csp_nonce</span><span class="w"> </span><span class="k" data-group-id="8464535524-38">do</span><span class="w">
    </span><span class="mi">24</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="nc">:crypto</span><span class="o">.</span><span class="n">strong_rand_bytes</span><span class="p" data-group-id="8464535524-39">(</span><span class="p" data-group-id="8464535524-39">)</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="nc">Base</span><span class="o">.</span><span class="n">encode64</span><span class="p" data-group-id="8464535524-40">(</span><span class="ss">padding</span><span class="p">:</span><span class="w"> </span><span class="no">false</span><span class="p" data-group-id="8464535524-40">)</span><span class="w">
  </span><span class="k" data-group-id="8464535524-38">end</span><span class="w">

  </span><span class="na">@doc</span><span class="w"> </span><span class="s">&quot;&quot;&quot;
  Loads the CSP nonce into the LiveView process.
  &quot;&quot;&quot;</span><span class="w">
  </span><span class="kd">def</span><span class="w"> </span><span class="nf">on_mount</span><span class="p" data-group-id="8464535524-41">(</span><span class="ss">:default</span><span class="p">,</span><span class="w"> </span><span class="c">_params</span><span class="p">,</span><span class="w"> </span><span class="c">_session</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="8464535524-42">%{</span><span class="ss">private</span><span class="p">:</span><span class="w"> </span><span class="p" data-group-id="8464535524-43">%{</span><span class="ss">connect_params</span><span class="p">:</span><span class="w"> </span><span class="p" data-group-id="8464535524-44">%{</span><span class="s">&quot;_csp_nonce&quot;</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="n">nonce</span><span class="p" data-group-id="8464535524-44">}</span><span class="p" data-group-id="8464535524-43">}</span><span class="p" data-group-id="8464535524-42">}</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">socket</span><span class="p" data-group-id="8464535524-41">)</span><span class="w"> </span><span class="k" data-group-id="8464535524-45">do</span><span class="w">
    </span><span class="nc">Process</span><span class="o">.</span><span class="n">put</span><span class="p" data-group-id="8464535524-46">(</span><span class="ss">:plug_csp_nonce</span><span class="p">,</span><span class="w"> </span><span class="n">nonce</span><span class="p" data-group-id="8464535524-46">)</span><span class="w">

    </span><span class="p" data-group-id="8464535524-47">{</span><span class="ss">:cont</span><span class="p">,</span><span class="w"> </span><span class="n">socket</span><span class="p" data-group-id="8464535524-47">}</span><span class="w">
  </span><span class="k" data-group-id="8464535524-45">end</span><span class="w">

  </span><span class="kd">def</span><span class="w"> </span><span class="nf">on_mount</span><span class="p" data-group-id="8464535524-48">(</span><span class="ss">:default</span><span class="p">,</span><span class="w"> </span><span class="c">_params</span><span class="p">,</span><span class="w"> </span><span class="c">_session</span><span class="p">,</span><span class="w"> </span><span class="n">socket</span><span class="p" data-group-id="8464535524-48">)</span><span class="w"> </span><span class="k" data-group-id="8464535524-49">do</span><span class="w">
    </span><span class="k">unless</span><span class="w"> </span><span class="nc">Process</span><span class="o">.</span><span class="n">get</span><span class="p" data-group-id="8464535524-50">(</span><span class="ss">:plug_csp_nonce</span><span class="p" data-group-id="8464535524-50">)</span><span class="w"> </span><span class="k" data-group-id="8464535524-51">do</span><span class="w">
      </span><span class="nc">Logger</span><span class="o">.</span><span class="n">debug</span><span class="p" data-group-id="8464535524-52">(</span><span class="s">&quot;&quot;&quot;
      LiveView session was misconfigured.

      1) Ensure the `put_content_security_policy` plug is in your router pipeline:

          plug :put_content_security_policy

      2) Define the CSRF meta tag inside the `&lt;head&gt;` tag in your layout:

          &lt;meta name=&quot;csp-nonce&quot; content={MyAppWeb.CSP.get_csp_nonce()} /&gt;

      3) Pass it forward in your app.js:

          let csrfToken = document.querySelector(&quot;meta[name=&#39;csp-nonce&#39;]&quot;).getAttribute(&quot;content&quot;);
          let liveSocket = new LiveSocket(&quot;/live&quot;, Socket, {params: {_csp_nonce: cspNonce}});
      &quot;&quot;&quot;</span><span class="p" data-group-id="8464535524-52">)</span><span class="w">
    </span><span class="k" data-group-id="8464535524-51">end</span><span class="w">

    </span><span class="p" data-group-id="8464535524-53">{</span><span class="ss">:cont</span><span class="p">,</span><span class="w"> </span><span class="n">socket</span><span class="p" data-group-id="8464535524-53">}</span><span class="w">
  </span><span class="k" data-group-id="8464535524-49">end</span><span class="w">
</span><span class="k" data-group-id="8464535524-1">end</span></code></pre>
<p>
The module documents how to install the CSP helpers in your Phoenix app. This is how I’m using the plug helper in my router pipeline:</p>
<pre><code class="makeup elixir"><span class="n">pipeline</span><span class="w"> </span><span class="ss">:browser</span><span class="w"> </span><span class="k" data-group-id="8482350607-1">do</span><span class="w">
  </span><span class="n">plug</span><span class="w"> </span><span class="ss">:accepts</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="8482350607-2">[</span><span class="s">&quot;html&quot;</span><span class="p" data-group-id="8482350607-2">]</span><span class="w">
  </span><span class="n">plug</span><span class="w"> </span><span class="ss">:fetch_session</span><span class="w">
  </span><span class="n">plug</span><span class="w"> </span><span class="ss">:fetch_live_flash</span><span class="w">
  </span><span class="n">plug</span><span class="w"> </span><span class="ss">:put_root_layout</span><span class="p">,</span><span class="w"> </span><span class="ss">html</span><span class="p">:</span><span class="w"> </span><span class="p" data-group-id="8482350607-3">{</span><span class="nc">MyAppWeb.Layouts</span><span class="p">,</span><span class="w"> </span><span class="ss">:root</span><span class="p" data-group-id="8482350607-3">}</span><span class="w">
  </span><span class="n">plug</span><span class="w"> </span><span class="ss">:protect_from_forgery</span><span class="w">
  </span><span class="n">plug</span><span class="w"> </span><span class="ss">:put_secure_browser_headers</span><span class="w">

  </span><span class="n">plug</span><span class="w"> </span><span class="ss">:put_content_security_policy</span><span class="p">,</span><span class="w">
    </span><span class="ss">img_src</span><span class="p">:</span><span class="w"> </span><span class="s">&quot;&#39;self&#39; data:&quot;</span><span class="p">,</span><span class="w">
    </span><span class="ss">style_src</span><span class="p">:</span><span class="w"> </span><span class="s">&quot;&#39;self&#39; &#39;nonce&#39;&quot;</span><span class="w">

  </span><span class="n">plug</span><span class="w"> </span><span class="ss">:load_current_user</span><span class="w">
</span><span class="k" data-group-id="8482350607-1">end</span></code></pre>
<p>
Unfortunately, <code class="inline">sobelow</code> can’t skip plugs with inline skip comment so we must run <code class="inline">mix sobelow --mark-skip-all</code> to generate a <code class="inline">.sobelow-skips</code> file to skip the error in subsequent runs.</p>
<p>
If you need to dynamically set CSP headers you can pass in a function:</p>
<pre><code class="makeup elixir"><span class="n">plug</span><span class="w"> </span><span class="ss">:put_content_security_policy</span><span class="p">,</span><span class="w"> </span><span class="o">&amp;</span><span class="nc">MyAppWeb.CSPPolicy</span><span class="o">.</span><span class="n">policy</span><span class="o">/</span><span class="mi">1</span></code></pre>
<pre><code class="makeup elixir"><span class="kd">defmodule</span><span class="w"> </span><span class="nc">MyAppWeb.CSPPolicy</span><span class="w"> </span><span class="k" data-group-id="2084605040-1">do</span><span class="w">
  </span><span class="na">@moduledoc</span><span class="w"> </span><span class="no">false</span><span class="w">

  </span><span class="kd">def</span><span class="w"> </span><span class="nf">policy</span><span class="p" data-group-id="2084605040-2">(</span><span class="c">_conn</span><span class="p" data-group-id="2084605040-2">)</span><span class="w"> </span><span class="k" data-group-id="2084605040-3">do</span><span class="w">
    </span><span class="k">if</span><span class="w"> </span><span class="n">dsn</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">Application</span><span class="o">.</span><span class="n">get_env</span><span class="p" data-group-id="2084605040-4">(</span><span class="ss">:sentry</span><span class="p">,</span><span class="w"> </span><span class="ss">:dsn</span><span class="p" data-group-id="2084605040-4">)</span><span class="w"> </span><span class="k" data-group-id="2084605040-5">do</span><span class="w">
      </span><span class="n">dsn</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">URI</span><span class="o">.</span><span class="n">parse</span><span class="p" data-group-id="2084605040-6">(</span><span class="n">dsn</span><span class="p" data-group-id="2084605040-6">)</span><span class="w">

      </span><span class="p" data-group-id="2084605040-7">[</span><span class="w">
        </span><span class="ss">script_src</span><span class="p">:</span><span class="w"> </span><span class="p" data-group-id="2084605040-8">[</span><span class="w">
          </span><span class="s">&quot;https://js.sentry-cdn.com/&quot;</span><span class="p">,</span><span class="w">
          </span><span class="s">&quot;https://browser.sentry-cdn.com/&quot;</span><span class="p">,</span><span class="w">
          </span><span class="s">&quot;blob:&quot;</span><span class="w">
        </span><span class="p" data-group-id="2084605040-8">]</span><span class="p">,</span><span class="w">
        </span><span class="ss">connect_src</span><span class="p">:</span><span class="w"> </span><span class="p" data-group-id="2084605040-9">[</span><span class="w">
          </span><span class="nc">URI</span><span class="o">.</span><span class="n">to_string</span><span class="p" data-group-id="2084605040-10">(</span><span class="p" data-group-id="2084605040-11">%{</span><span class="n">dsn</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="ss">userinfo</span><span class="p">:</span><span class="w"> </span><span class="no">nil</span><span class="p">,</span><span class="w"> </span><span class="ss">path</span><span class="p">:</span><span class="w"> </span><span class="s">&quot;/api</span><span class="si" data-group-id="2084605040-12">#{</span><span class="n">dsn</span><span class="o">.</span><span class="n">path</span><span class="si" data-group-id="2084605040-12">}</span><span class="s">/&quot;</span><span class="p" data-group-id="2084605040-11">}</span><span class="p" data-group-id="2084605040-10">)</span><span class="w">
        </span><span class="p" data-group-id="2084605040-9">]</span><span class="w">
      </span><span class="p" data-group-id="2084605040-7">]</span><span class="w">
    </span><span class="k" data-group-id="2084605040-5">else</span><span class="w">
      </span><span class="p" data-group-id="2084605040-13">[</span><span class="p" data-group-id="2084605040-13">]</span><span class="w">
    </span><span class="k" data-group-id="2084605040-5">end</span><span class="w">
  </span><span class="k" data-group-id="2084605040-3">end</span><span class="w">
</span><span class="k" data-group-id="2084605040-1">end</span></code></pre>
<h3>
Considerations</h3>
<p>
There is a potential security risk using nonce here; an attacker may find a way to inject payload through the LiveView socket with the DOM manipulation. That could be in unescaped user content pushed to a <code class="inline">&lt;style&gt;</code> or <code class="inline">&lt;script&gt;</code> tag. The hash eliminates these risks, and you should opt for no inline content if possible.</p>
<details><summary class="cursor-pointer">
Tests for <code class="inline">MyAppWeb.CSP</code>.
</summary>
<pre><code class="makeup elixir"><span class="kd">defmodule</span><span class="w"> </span><span class="nc">MyAppWeb.CSPTest</span><span class="w"> </span><span class="k" data-group-id="4466754006-1">do</span><span class="w">
  </span><span class="kn">use</span><span class="w"> </span><span class="nc">MyAppWeb.ConnCase</span><span class="w">

  </span><span class="kn">alias</span><span class="w"> </span><span class="nc">MyAppWeb.CSP</span><span class="w">

  </span><span class="n">describe</span><span class="w"> </span><span class="s">&quot;put_content_security_policy/2&quot;</span><span class="w"> </span><span class="k" data-group-id="4466754006-2">do</span><span class="w">
    </span><span class="n">test</span><span class="w"> </span><span class="s">&quot;sets default CSP header&quot;</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="4466754006-3">%{</span><span class="ss">conn</span><span class="p">:</span><span class="w"> </span><span class="n">conn</span><span class="p" data-group-id="4466754006-3">}</span><span class="w"> </span><span class="k" data-group-id="4466754006-4">do</span><span class="w">
      </span><span class="n">conn</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">CSP</span><span class="o">.</span><span class="n">put_content_security_policy</span><span class="p" data-group-id="4466754006-5">(</span><span class="n">conn</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="4466754006-6">[</span><span class="p" data-group-id="4466754006-6">]</span><span class="p" data-group-id="4466754006-5">)</span><span class="w">

      </span><span class="n">assert</span><span class="w"> </span><span class="n">get_resp_header</span><span class="p" data-group-id="4466754006-7">(</span><span class="n">conn</span><span class="p">,</span><span class="w"> </span><span class="s">&quot;content-security-policy&quot;</span><span class="p" data-group-id="4466754006-7">)</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="p" data-group-id="4466754006-8">[</span><span class="s">&quot;default-src &#39;self&#39;;&quot;</span><span class="p" data-group-id="4466754006-8">]</span><span class="w">
    </span><span class="k" data-group-id="4466754006-4">end</span><span class="w">

    </span><span class="n">test</span><span class="w"> </span><span class="s">&quot;with options&quot;</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="4466754006-9">%{</span><span class="ss">conn</span><span class="p">:</span><span class="w"> </span><span class="n">conn</span><span class="p" data-group-id="4466754006-9">}</span><span class="w"> </span><span class="k" data-group-id="4466754006-10">do</span><span class="w">
      </span><span class="n">conn</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">CSP</span><span class="o">.</span><span class="n">put_content_security_policy</span><span class="p" data-group-id="4466754006-11">(</span><span class="n">conn</span><span class="p">,</span><span class="w"> </span><span class="ss">img_src</span><span class="p">:</span><span class="w"> </span><span class="s">&quot;&#39;self&#39; data:&quot;</span><span class="p" data-group-id="4466754006-11">)</span><span class="w">

      </span><span class="n">assert</span><span class="w"> </span><span class="n">get_resp_header</span><span class="p" data-group-id="4466754006-12">(</span><span class="n">conn</span><span class="p">,</span><span class="w"> </span><span class="s">&quot;content-security-policy&quot;</span><span class="p" data-group-id="4466754006-12">)</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="p" data-group-id="4466754006-13">[</span><span class="s">&quot;default-src &#39;self&#39;;img-src &#39;self&#39; data:;&quot;</span><span class="p" data-group-id="4466754006-13">]</span><span class="w">
    </span><span class="k" data-group-id="4466754006-10">end</span><span class="w">

    </span><span class="n">test</span><span class="w"> </span><span class="s">&quot;with list of sources&quot;</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="4466754006-14">%{</span><span class="ss">conn</span><span class="p">:</span><span class="w"> </span><span class="n">conn</span><span class="p" data-group-id="4466754006-14">}</span><span class="w"> </span><span class="k" data-group-id="4466754006-15">do</span><span class="w">
      </span><span class="n">conn</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">CSP</span><span class="o">.</span><span class="n">put_content_security_policy</span><span class="p" data-group-id="4466754006-16">(</span><span class="n">conn</span><span class="p">,</span><span class="w"> </span><span class="ss">default_src</span><span class="p">:</span><span class="w"> </span><span class="p" data-group-id="4466754006-17">[</span><span class="s">&quot;&#39;self&#39;&quot;</span><span class="p">,</span><span class="w"> </span><span class="s">&quot;data:&quot;</span><span class="p" data-group-id="4466754006-17">]</span><span class="p" data-group-id="4466754006-16">)</span><span class="w">

      </span><span class="n">assert</span><span class="w"> </span><span class="n">get_resp_header</span><span class="p" data-group-id="4466754006-18">(</span><span class="n">conn</span><span class="p">,</span><span class="w"> </span><span class="s">&quot;content-security-policy&quot;</span><span class="p" data-group-id="4466754006-18">)</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="p" data-group-id="4466754006-19">[</span><span class="s">&quot;default-src &#39;self&#39; data:;&quot;</span><span class="p" data-group-id="4466754006-19">]</span><span class="w">
    </span><span class="k" data-group-id="4466754006-15">end</span><span class="w">

    </span><span class="n">test</span><span class="w"> </span><span class="s">&quot;with duplicate directives&quot;</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="4466754006-20">%{</span><span class="ss">conn</span><span class="p">:</span><span class="w"> </span><span class="n">conn</span><span class="p" data-group-id="4466754006-20">}</span><span class="w"> </span><span class="k" data-group-id="4466754006-21">do</span><span class="w">
      </span><span class="n">conn</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">CSP</span><span class="o">.</span><span class="n">put_content_security_policy</span><span class="p" data-group-id="4466754006-22">(</span><span class="n">conn</span><span class="p">,</span><span class="w"> </span><span class="ss">default_src</span><span class="p">:</span><span class="w"> </span><span class="s">&quot;&#39;self&#39;&quot;</span><span class="p">,</span><span class="w"> </span><span class="ss">default_src</span><span class="p">:</span><span class="w"> </span><span class="s">&quot;data:&quot;</span><span class="p" data-group-id="4466754006-22">)</span><span class="w">

      </span><span class="n">assert</span><span class="w"> </span><span class="n">get_resp_header</span><span class="p" data-group-id="4466754006-23">(</span><span class="n">conn</span><span class="p">,</span><span class="w"> </span><span class="s">&quot;content-security-policy&quot;</span><span class="p" data-group-id="4466754006-23">)</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="p" data-group-id="4466754006-24">[</span><span class="s">&quot;default-src &#39;self&#39; data:;&quot;</span><span class="p" data-group-id="4466754006-24">]</span><span class="w">
    </span><span class="k" data-group-id="4466754006-21">end</span><span class="w">

    </span><span class="n">test</span><span class="w"> </span><span class="s">&quot;with duplicate sources&quot;</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="4466754006-25">%{</span><span class="ss">conn</span><span class="p">:</span><span class="w"> </span><span class="n">conn</span><span class="p" data-group-id="4466754006-25">}</span><span class="w"> </span><span class="k" data-group-id="4466754006-26">do</span><span class="w">
      </span><span class="n">conn</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">CSP</span><span class="o">.</span><span class="n">put_content_security_policy</span><span class="p" data-group-id="4466754006-27">(</span><span class="n">conn</span><span class="p">,</span><span class="w"> </span><span class="ss">default_src</span><span class="p">:</span><span class="w"> </span><span class="p" data-group-id="4466754006-28">[</span><span class="s">&quot;&#39;self&#39;&quot;</span><span class="p">,</span><span class="w"> </span><span class="s">&quot;data:&quot;</span><span class="p" data-group-id="4466754006-28">]</span><span class="p">,</span><span class="w"> </span><span class="ss">default_src</span><span class="p">:</span><span class="w"> </span><span class="s">&quot;data:&quot;</span><span class="p" data-group-id="4466754006-27">)</span><span class="w">

      </span><span class="n">assert</span><span class="w"> </span><span class="n">get_resp_header</span><span class="p" data-group-id="4466754006-29">(</span><span class="n">conn</span><span class="p">,</span><span class="w"> </span><span class="s">&quot;content-security-policy&quot;</span><span class="p" data-group-id="4466754006-29">)</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="p" data-group-id="4466754006-30">[</span><span class="s">&quot;default-src &#39;self&#39; data:;&quot;</span><span class="p" data-group-id="4466754006-30">]</span><span class="w">
    </span><span class="k" data-group-id="4466754006-26">end</span><span class="w">

    </span><span class="n">test</span><span class="w"> </span><span class="s">&quot;with nonce source&quot;</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="4466754006-31">%{</span><span class="ss">conn</span><span class="p">:</span><span class="w"> </span><span class="n">conn</span><span class="p" data-group-id="4466754006-31">}</span><span class="w"> </span><span class="k" data-group-id="4466754006-32">do</span><span class="w">
      </span><span class="n">conn</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">CSP</span><span class="o">.</span><span class="n">put_content_security_policy</span><span class="p" data-group-id="4466754006-33">(</span><span class="n">conn</span><span class="p">,</span><span class="w"> </span><span class="ss">default_src</span><span class="p">:</span><span class="w"> </span><span class="s">&quot;&#39;self&#39; &#39;nonce&#39;&quot;</span><span class="p" data-group-id="4466754006-33">)</span><span class="w">

      </span><span class="n">assert</span><span class="w"> </span><span class="p" data-group-id="4466754006-34">[</span><span class="s">&quot;default-src &#39;self&#39; &#39;nonce-&quot;</span><span class="w"> </span><span class="o">&lt;&gt;</span><span class="w"> </span><span class="c">_nonce</span><span class="p" data-group-id="4466754006-34">]</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">get_resp_header</span><span class="p" data-group-id="4466754006-35">(</span><span class="n">conn</span><span class="p">,</span><span class="w"> </span><span class="s">&quot;content-security-policy&quot;</span><span class="p" data-group-id="4466754006-35">)</span><span class="w">
    </span><span class="k" data-group-id="4466754006-32">end</span><span class="w">

    </span><span class="kd">defp</span><span class="w"> </span><span class="nf">my_function</span><span class="p" data-group-id="4466754006-36">(</span><span class="n">conn</span><span class="p" data-group-id="4466754006-36">)</span><span class="w"> </span><span class="k" data-group-id="4466754006-37">do</span><span class="w">
      </span><span class="p" data-group-id="4466754006-38">[</span><span class="w">
        </span><span class="ss">default_src</span><span class="p">:</span><span class="w"> </span><span class="n">conn</span><span class="o">.</span><span class="n">host</span><span class="w">
      </span><span class="p" data-group-id="4466754006-38">]</span><span class="w">
    </span><span class="k" data-group-id="4466754006-37">end</span><span class="w">

    </span><span class="n">test</span><span class="w"> </span><span class="s">&quot;with function&quot;</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="4466754006-39">%{</span><span class="ss">conn</span><span class="p">:</span><span class="w"> </span><span class="n">conn</span><span class="p" data-group-id="4466754006-39">}</span><span class="w"> </span><span class="k" data-group-id="4466754006-40">do</span><span class="w">
      </span><span class="n">conn</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">CSP</span><span class="o">.</span><span class="n">put_content_security_policy</span><span class="p" data-group-id="4466754006-41">(</span><span class="n">conn</span><span class="p">,</span><span class="w"> </span><span class="o">&amp;</span><span class="n">my_function</span><span class="o">/</span><span class="mi">1</span><span class="p" data-group-id="4466754006-41">)</span><span class="w">

      </span><span class="n">assert</span><span class="w"> </span><span class="n">get_resp_header</span><span class="p" data-group-id="4466754006-42">(</span><span class="n">conn</span><span class="p">,</span><span class="w"> </span><span class="s">&quot;content-security-policy&quot;</span><span class="p" data-group-id="4466754006-42">)</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="p" data-group-id="4466754006-43">[</span><span class="s">&quot;default-src </span><span class="si" data-group-id="4466754006-44">#{</span><span class="n">conn</span><span class="o">.</span><span class="n">host</span><span class="si" data-group-id="4466754006-44">}</span><span class="s">;&quot;</span><span class="p" data-group-id="4466754006-43">]</span><span class="w">
    </span><span class="k" data-group-id="4466754006-40">end</span><span class="w">
  </span><span class="k" data-group-id="4466754006-2">end</span><span class="w">

  </span><span class="n">describe</span><span class="w"> </span><span class="s">&quot;get_csp_nonce/0&quot;</span><span class="w"> </span><span class="k" data-group-id="4466754006-45">do</span><span class="w">
    </span><span class="n">test</span><span class="w"> </span><span class="s">&quot;token has no padding&quot;</span><span class="w"> </span><span class="k" data-group-id="4466754006-46">do</span><span class="w">
      </span><span class="n">refute</span><span class="w"> </span><span class="nc">CSP</span><span class="o">.</span><span class="n">get_csp_nonce</span><span class="p" data-group-id="4466754006-47">(</span><span class="p" data-group-id="4466754006-47">)</span><span class="w"> </span><span class="o">=~</span><span class="w"> </span><span class="s">&quot;=&quot;</span><span class="w">
    </span><span class="k" data-group-id="4466754006-46">end</span><span class="w">

    </span><span class="n">test</span><span class="w"> </span><span class="s">&quot;token is stored in process dictionary&quot;</span><span class="w"> </span><span class="k" data-group-id="4466754006-48">do</span><span class="w">
      </span><span class="n">assert</span><span class="w"> </span><span class="nc">CSP</span><span class="o">.</span><span class="n">get_csp_nonce</span><span class="p" data-group-id="4466754006-49">(</span><span class="p" data-group-id="4466754006-49">)</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="nc">CSP</span><span class="o">.</span><span class="n">get_csp_nonce</span><span class="p" data-group-id="4466754006-50">(</span><span class="p" data-group-id="4466754006-50">)</span><span class="w">

      </span><span class="n">token</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">CSP</span><span class="o">.</span><span class="n">get_csp_nonce</span><span class="p" data-group-id="4466754006-51">(</span><span class="p" data-group-id="4466754006-51">)</span><span class="w">
      </span><span class="nc">Process</span><span class="o">.</span><span class="n">delete</span><span class="p" data-group-id="4466754006-52">(</span><span class="ss">:plug_csp_nonce</span><span class="p" data-group-id="4466754006-52">)</span><span class="w">
      </span><span class="n">assert</span><span class="w"> </span><span class="n">token</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="nc">CSP</span><span class="o">.</span><span class="n">get_csp_nonce</span><span class="p" data-group-id="4466754006-53">(</span><span class="p" data-group-id="4466754006-53">)</span><span class="w">
    </span><span class="k" data-group-id="4466754006-48">end</span><span class="w">
  </span><span class="k" data-group-id="4466754006-45">end</span><span class="w">
</span><span class="k" data-group-id="4466754006-1">end</span></code></pre>
</details>

      ]]>
    </content>
  </entry>
  
  <entry>
    <title>CSV Export using NimbleCSV</title>
    <link href="https://danschultzer.com/posts/csv-export-using-nimblecsv" />
    <id>https://danschultzer.com/posts/csv-export-using-nimblecsv</id>
    <updated>2024-01-17T00:00:00Z</updated>
    <summary>Using NimbleCSV to export large CSV files.</summary>
    <content type="html">
      <![CDATA[
        <p>
Using <a href="https://hex.pm/packages/nimble_csv"><code class="inline">NimbleCSV</code></a> to export large CSV files is simple, but I encountered a few issues along the way as the size of exported data grew. I’ll go through a few iterations I saw, starting with the most naive implementation.</p>
<p>
Let us begin with streaming data from our database:</p>
<pre><code class="makeup elixir"><span class="kd">defmodule</span><span class="w"> </span><span class="nc">MyApp.Resources</span><span class="w"> </span><span class="k" data-group-id="7780423989-1">do</span><span class="w">
  </span><span class="kn">alias</span><span class="w"> </span><span class="nc">MyApp</span><span class="o">.</span><span class="p" data-group-id="7780423989-2">{</span><span class="nc">Repo</span><span class="p">,</span><span class="w"> </span><span class="nc">Resources.Resource</span><span class="p" data-group-id="7780423989-2">}</span><span class="w">

  </span><span class="kd">def</span><span class="w"> </span><span class="nf">stream_resources</span><span class="p" data-group-id="7780423989-3">(</span><span class="n">opts</span><span class="w"> </span><span class="o">\\</span><span class="w"> </span><span class="p" data-group-id="7780423989-4">[</span><span class="p" data-group-id="7780423989-4">]</span><span class="p" data-group-id="7780423989-3">)</span><span class="w"> </span><span class="k" data-group-id="7780423989-5">do</span><span class="w">
    </span><span class="nc">Repo</span><span class="o">.</span><span class="n">stream</span><span class="p" data-group-id="7780423989-6">(</span><span class="nc">Resource</span><span class="p">,</span><span class="w"> </span><span class="n">opts</span><span class="p" data-group-id="7780423989-6">)</span><span class="w">
  </span><span class="k" data-group-id="7780423989-5">end</span><span class="w">
</span><span class="k" data-group-id="7780423989-1">end</span></code></pre>
<p>
With this, we’ll now set up the controller action to export our CSV:</p>
<pre><code class="makeup elixir"><span class="kd">defmodule</span><span class="w"> </span><span class="nc">MyAppWeb.CSVController</span><span class="w"> </span><span class="k" data-group-id="2196094670-1">do</span><span class="w">
  </span><span class="kn">use</span><span class="w"> </span><span class="nc">MyAppWeb</span><span class="p">,</span><span class="w"> </span><span class="ss">:controller</span><span class="w">

  </span><span class="kn">alias</span><span class="w"> </span><span class="nc">NimbleCSV.RFC4180</span><span class="p">,</span><span class="w"> </span><span class="ss">as</span><span class="p">:</span><span class="w"> </span><span class="nc">CSV</span><span class="w">
  </span><span class="kn">alias</span><span class="w"> </span><span class="nc">MyApp</span><span class="o">.</span><span class="p" data-group-id="2196094670-2">{</span><span class="nc">Resources</span><span class="p">,</span><span class="w"> </span><span class="nc">Repo</span><span class="p" data-group-id="2196094670-2">}</span><span class="w">

  </span><span class="kd">def</span><span class="w"> </span><span class="nf">export</span><span class="p" data-group-id="2196094670-3">(</span><span class="n">conn</span><span class="p">,</span><span class="w"> </span><span class="c">_params</span><span class="p" data-group-id="2196094670-3">)</span><span class="w"> </span><span class="k" data-group-id="2196094670-4">do</span><span class="w">
    </span><span class="n">conn</span><span class="w"> </span><span class="o">=</span><span class="w">
      </span><span class="n">conn</span><span class="w">
      </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">put_resp_content_type</span><span class="p" data-group-id="2196094670-5">(</span><span class="s">&quot;text/csv&quot;</span><span class="p" data-group-id="2196094670-5">)</span><span class="w">
      </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">put_resp_header</span><span class="p" data-group-id="2196094670-6">(</span><span class="s">&quot;content-disposition&quot;</span><span class="p">,</span><span class="w"> </span><span class="s">&quot;attachment; filename=</span><span class="se">\&quot;</span><span class="s">export.csv</span><span class="se">\&quot;</span><span class="s">&quot;</span><span class="p" data-group-id="2196094670-6">)</span><span class="w">
      </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">send_chunked</span><span class="p" data-group-id="2196094670-7">(</span><span class="ss">:ok</span><span class="p" data-group-id="2196094670-7">)</span><span class="w">

    </span><span class="c1"># Dump header</span><span class="w">
    </span><span class="n">rows</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">CSV</span><span class="o">.</span><span class="n">dump_to_iodata</span><span class="p" data-group-id="2196094670-8">(</span><span class="p" data-group-id="2196094670-9">[</span><span class="p" data-group-id="2196094670-10">[</span><span class="s">&quot;id&quot;</span><span class="p">,</span><span class="w"> </span><span class="s">&quot;field1&quot;</span><span class="p">,</span><span class="w"> </span><span class="s">&quot;field2&quot;</span><span class="p" data-group-id="2196094670-10">]</span><span class="p" data-group-id="2196094670-9">]</span><span class="p" data-group-id="2196094670-8">)</span><span class="w">
    </span><span class="p" data-group-id="2196094670-11">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="c">_conn</span><span class="p" data-group-id="2196094670-11">}</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">chunk</span><span class="p" data-group-id="2196094670-12">(</span><span class="n">conn</span><span class="p">,</span><span class="w"> </span><span class="n">rows</span><span class="p" data-group-id="2196094670-12">)</span><span class="w">

    </span><span class="c1"># Stream data</span><span class="w">
    </span><span class="nc">Repo</span><span class="o">.</span><span class="n">transaction</span><span class="p" data-group-id="2196094670-13">(</span><span class="k" data-group-id="2196094670-14">fn</span><span class="w"> </span><span class="o">-&gt;</span><span class="w">
      </span><span class="n">rows</span><span class="w"> </span><span class="o">=</span><span class="w">
        </span><span class="nc">Resources</span><span class="o">.</span><span class="n">stream_resources</span><span class="p" data-group-id="2196094670-15">(</span><span class="p" data-group-id="2196094670-15">)</span><span class="w">
        </span><span class="o">|&gt;</span><span class="w"> </span><span class="nc">Enum</span><span class="o">.</span><span class="n">map</span><span class="p" data-group-id="2196094670-16">(</span><span class="k" data-group-id="2196094670-17">fn</span><span class="w"> </span><span class="n">resource</span><span class="w"> </span><span class="o">-&gt;</span><span class="w">
          </span><span class="c1"># Do something potentially expensive here</span><span class="w">
          </span><span class="c1"># [id, value1, value2]</span><span class="w">
        </span><span class="k" data-group-id="2196094670-17">end</span><span class="p" data-group-id="2196094670-16">)</span><span class="w">
        </span><span class="o">|&gt;</span><span class="w"> </span><span class="nc">CSV</span><span class="o">.</span><span class="n">dump_to_iodata</span><span class="p" data-group-id="2196094670-18">(</span><span class="p" data-group-id="2196094670-18">)</span><span class="w">

      </span><span class="p" data-group-id="2196094670-19">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="c">_conn</span><span class="p" data-group-id="2196094670-19">}</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">chunk</span><span class="p" data-group-id="2196094670-20">(</span><span class="n">conn</span><span class="p">,</span><span class="w"> </span><span class="n">rows</span><span class="p" data-group-id="2196094670-20">)</span><span class="w">
    </span><span class="k" data-group-id="2196094670-14">end</span><span class="p" data-group-id="2196094670-13">)</span><span class="w">
  </span><span class="k" data-group-id="2196094670-4">end</span><span class="w">

  </span><span class="n">conn</span><span class="w">
</span><span class="k" data-group-id="2196094670-1">end</span></code></pre>
<p>
Immediately there are obvious issues here. Can you spot them?</p>
<p>
<code class="inline">Enum.map/2</code> is <a href="https://hexdocs.pm/elixir/Enum.html">greedy</a>, this means that all data will be in memory (the same is true with the <code class="inline">rows</code> assign). In the above the chunk sent will be ALL data! Chunking the response doesn’t make sense if we do that.</p>
<p>
Instead of <code class="inline">Enum.map/2</code> we should be using <a href="https://hexdocs.pm/elixir/Stream.html#each/2"><code class="inline">Stream.each/2</code></a> or <a href="https://hexdocs.pm/elixir/Stream.html#transform/3"><code class="inline">Stream.transform/3</code></a> dumping data when transforming each row:</p>
<pre><code class="makeup elixir"><span class="c1"># Stream data</span><span class="w">
</span><span class="nc">Repo</span><span class="o">.</span><span class="n">transaction</span><span class="p" data-group-id="8491253472-1">(</span><span class="k" data-group-id="8491253472-2">fn</span><span class="w"> </span><span class="o">-&gt;</span><span class="w">
  </span><span class="nc">Resources</span><span class="o">.</span><span class="n">stream_resources</span><span class="p" data-group-id="8491253472-3">(</span><span class="p" data-group-id="8491253472-3">)</span><span class="w">
  </span><span class="o">|&gt;</span><span class="w"> </span><span class="nc">Stream</span><span class="o">.</span><span class="n">each</span><span class="p" data-group-id="8491253472-4">(</span><span class="k" data-group-id="8491253472-5">fn</span><span class="w"> </span><span class="n">resource</span><span class="w"> </span><span class="o">-&gt;</span><span class="w">
    </span><span class="c1"># Do something potentially expensive here</span><span class="w">
    </span><span class="c1"># row = [id, value1, value2]</span><span class="w">

    </span><span class="p" data-group-id="8491253472-6">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="c">_conn</span><span class="p" data-group-id="8491253472-6">}</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">chunk</span><span class="p" data-group-id="8491253472-7">(</span><span class="n">conn</span><span class="p">,</span><span class="w"> </span><span class="nc">CSV</span><span class="o">.</span><span class="n">dump_to_iodata</span><span class="p" data-group-id="8491253472-8">(</span><span class="p" data-group-id="8491253472-9">[</span><span class="n">row</span><span class="p" data-group-id="8491253472-9">]</span><span class="p" data-group-id="8491253472-8">)</span><span class="p" data-group-id="8491253472-7">)</span><span class="w">
  </span><span class="k" data-group-id="8491253472-5">end</span><span class="p" data-group-id="8491253472-4">)</span><span class="w">
  </span><span class="o">|&gt;</span><span class="w"> </span><span class="nc">Stream</span><span class="o">.</span><span class="n">run</span><span class="p" data-group-id="8491253472-10">(</span><span class="p" data-group-id="8491253472-10">)</span><span class="w">
</span><span class="k" data-group-id="8491253472-2">end</span><span class="p" data-group-id="8491253472-1">)</span></code></pre>
<p>
Now each row is pushed as a chunk.</p>
<h2>
DBConnection timeouts</h2>
<p>
As we began exporting larger amounts of data (or more expensive data transformations) we experienced db connection timeouts:</p>
<pre><code>[error] Postgrex.Protocol (#PID&lt;0.2180.0&gt;) disconnected: ** (DBConnection.ConnectionError) client #PID&lt;0.1113969.0&gt; timed out because it queued and checked out the connection for longer than 15000ms	</code></pre>
<p>
Since we are streaming inside a transaction, which is required for <a href="https://hexdocs.pm/ecto/Ecto.Repo.html#c:stream/2"><code class="inline">Repo.stream/2</code></a>, we got a <a href="https://hexdocs.pm/ecto/Ecto.Repo.html#module-shared-options">default timeout of 15 seconds</a>. We could increase the timeout for <code class="inline">Repo.transaction/2</code>, but we are doing somewhat expensive data transformation for each row (in the order of 5-10+ ms), and we’re dealing with ever increasing datasets now in the hundreds of thousands of rows.</p>
<p>
I don’t want to just add <code class="inline">timeout: :infinity</code>, and I don’t think we need this to run in a transaction at all. We should stop using <code class="inline">Repo.stream/2</code> and instead paginate through the results:</p>
<pre><code class="makeup elixir"><span class="kd">def</span><span class="w"> </span><span class="nf">stream_resources</span><span class="p" data-group-id="6212453836-1">(</span><span class="n">opts</span><span class="w"> </span><span class="o">\\</span><span class="w"> </span><span class="p" data-group-id="6212453836-2">[</span><span class="p" data-group-id="6212453836-2">]</span><span class="p" data-group-id="6212453836-1">)</span><span class="w"> </span><span class="k" data-group-id="6212453836-3">do</span><span class="w">
  </span><span class="n">cursor_stream</span><span class="p" data-group-id="6212453836-4">(</span><span class="nc">Resource</span><span class="p">,</span><span class="w"> </span><span class="n">opts</span><span class="p" data-group-id="6212453836-4">)</span><span class="w">
</span><span class="k" data-group-id="6212453836-3">end</span><span class="w">

</span><span class="kd">defp</span><span class="w"> </span><span class="nf">cursor_stream</span><span class="p" data-group-id="6212453836-5">(</span><span class="n">query</span><span class="p">,</span><span class="w"> </span><span class="n">opts</span><span class="p" data-group-id="6212453836-5">)</span><span class="w"> </span><span class="k" data-group-id="6212453836-6">do</span><span class="w">
  </span><span class="n">starting_after</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">Keyword</span><span class="o">.</span><span class="n">get</span><span class="p" data-group-id="6212453836-7">(</span><span class="n">opts</span><span class="p">,</span><span class="w"> </span><span class="ss">:starting_after</span><span class="p" data-group-id="6212453836-7">)</span><span class="w">
  </span><span class="n">limit</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">Keyword</span><span class="o">.</span><span class="n">get</span><span class="p" data-group-id="6212453836-8">(</span><span class="n">opts</span><span class="p">,</span><span class="w"> </span><span class="ss">:max_rows</span><span class="p">,</span><span class="w"> </span><span class="mi">500</span><span class="p" data-group-id="6212453836-8">)</span><span class="w">

  </span><span class="n">starting_after</span><span class="w">
  </span><span class="o">|&gt;</span><span class="w"> </span><span class="nc">Stream</span><span class="o">.</span><span class="n">unfold</span><span class="p" data-group-id="6212453836-9">(</span><span class="k" data-group-id="6212453836-10">fn</span><span class="w"> </span><span class="n">starting_after</span><span class="w"> </span><span class="o">-&gt;</span><span class="w">
    </span><span class="k">case</span><span class="w"> </span><span class="n">stream_chunk</span><span class="p" data-group-id="6212453836-11">(</span><span class="n">query</span><span class="p">,</span><span class="w"> </span><span class="n">limit</span><span class="p">,</span><span class="w"> </span><span class="n">starting_after</span><span class="p" data-group-id="6212453836-11">)</span><span class="w"> </span><span class="k" data-group-id="6212453836-12">do</span><span class="w">
      </span><span class="p" data-group-id="6212453836-13">[</span><span class="p" data-group-id="6212453836-13">]</span><span class="w"> </span><span class="o">-&gt;</span><span class="w"> </span><span class="no">nil</span><span class="w">
      </span><span class="n">rows</span><span class="w"> </span><span class="o">-&gt;</span><span class="w"> </span><span class="p" data-group-id="6212453836-14">{</span><span class="n">rows</span><span class="p">,</span><span class="w"> </span><span class="nc">List</span><span class="o">.</span><span class="n">last</span><span class="p" data-group-id="6212453836-15">(</span><span class="n">rows</span><span class="p" data-group-id="6212453836-15">)</span><span class="p" data-group-id="6212453836-14">}</span><span class="w">
    </span><span class="k" data-group-id="6212453836-12">end</span><span class="w">
  </span><span class="k" data-group-id="6212453836-10">end</span><span class="p" data-group-id="6212453836-9">)</span><span class="w">
  </span><span class="o">|&gt;</span><span class="w"> </span><span class="nc">Stream</span><span class="o">.</span><span class="n">flat_map</span><span class="p" data-group-id="6212453836-16">(</span><span class="o">&amp;</span><span class="w"> </span><span class="ni">&amp;1</span><span class="p" data-group-id="6212453836-16">)</span><span class="w">
</span><span class="k" data-group-id="6212453836-6">end</span><span class="w">

</span><span class="kd">defp</span><span class="w"> </span><span class="nf">stream_chunk</span><span class="p" data-group-id="6212453836-17">(</span><span class="n">query</span><span class="p">,</span><span class="w"> </span><span class="n">limit</span><span class="p">,</span><span class="w"> </span><span class="n">starting_after</span><span class="p" data-group-id="6212453836-17">)</span><span class="w"> </span><span class="k" data-group-id="6212453836-18">do</span><span class="w">
  </span><span class="n">query</span><span class="w">
  </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">order_by</span><span class="p" data-group-id="6212453836-19">(</span><span class="ss">asc</span><span class="p">:</span><span class="w"> </span><span class="ss">:inserted_at</span><span class="p" data-group-id="6212453836-19">)</span><span class="w">
  </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">limit</span><span class="p" data-group-id="6212453836-20">(</span><span class="o">^</span><span class="n">limit</span><span class="p" data-group-id="6212453836-20">)</span><span class="w">
  </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">starting_after</span><span class="p" data-group-id="6212453836-21">(</span><span class="n">starting_after</span><span class="p" data-group-id="6212453836-21">)</span><span class="w">
  </span><span class="o">|&gt;</span><span class="w"> </span><span class="nc">Repo</span><span class="o">.</span><span class="n">all</span><span class="p" data-group-id="6212453836-22">(</span><span class="p" data-group-id="6212453836-22">)</span><span class="w">
</span><span class="k" data-group-id="6212453836-18">end</span><span class="w">

</span><span class="kd">defp</span><span class="w"> </span><span class="nf">starting_after</span><span class="p" data-group-id="6212453836-23">(</span><span class="n">query</span><span class="p">,</span><span class="w"> </span><span class="no">nil</span><span class="p" data-group-id="6212453836-23">)</span><span class="p">,</span><span class="w"> </span><span class="ss">do</span><span class="p">:</span><span class="w"> </span><span class="n">query</span><span class="w">

</span><span class="kd">defp</span><span class="w"> </span><span class="nf">starting_after</span><span class="p" data-group-id="6212453836-24">(</span><span class="n">query</span><span class="p">,</span><span class="w"> </span><span class="n">starting_after</span><span class="p" data-group-id="6212453836-24">)</span><span class="w"> </span><span class="k" data-group-id="6212453836-25">do</span><span class="w">
  </span><span class="n">where</span><span class="p" data-group-id="6212453836-26">(</span><span class="n">query</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="6212453836-27">[</span><span class="n">r</span><span class="p" data-group-id="6212453836-27">]</span><span class="p">,</span><span class="w"> </span><span class="n">r</span><span class="o">.</span><span class="n">id</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="o">^</span><span class="n">starting_after</span><span class="o">.</span><span class="n">id</span><span class="w"> </span><span class="ow">and</span><span class="w"> </span><span class="n">r</span><span class="o">.</span><span class="n">inserted_at</span><span class="w"> </span><span class="o">&gt;=</span><span class="w"> </span><span class="o">^</span><span class="n">starting_after</span><span class="o">.</span><span class="n">inserted_at</span><span class="p" data-group-id="6212453836-26">)</span><span class="w">
</span><span class="k" data-group-id="6212453836-25">end</span></code></pre>
<p>
In the above, we got cursor pagination to work as an Elixir stream so we can use it the same way we were using the <code class="inline">Repo.stream/2</code>. With this, we no longer see db connection timeouts!</p>
<h2>
Cowboy timeouts</h2>
<p>
However, we began to experience another issue. For some reason, the CSV just stopped streaming data after about a minute, with the connection being closed.</p>
<p>
This error doesn’t reveal itself at all. Everything runs as it should with no errors. The CSV file looks correct. So we should first make sure that the end-user can confirm whether they got the full dataset by adding comments to the header and footer of the CSV file:</p>
<pre><code class="makeup elixir"><span class="kd">def</span><span class="w"> </span><span class="nf">export</span><span class="p" data-group-id="1587397615-1">(</span><span class="n">conn</span><span class="p">,</span><span class="w"> </span><span class="c">_params</span><span class="p" data-group-id="1587397615-1">)</span><span class="w"> </span><span class="k" data-group-id="1587397615-2">do</span><span class="w">
  </span><span class="n">conn</span><span class="w"> </span><span class="o">=</span><span class="w">
    </span><span class="n">conn</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">put_resp_content_type</span><span class="p" data-group-id="1587397615-3">(</span><span class="s">&quot;text/csv&quot;</span><span class="p" data-group-id="1587397615-3">)</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">put_resp_header</span><span class="p" data-group-id="1587397615-4">(</span><span class="s">&quot;content-disposition&quot;</span><span class="p">,</span><span class="w"> </span><span class="s">&quot;attachment; filename=</span><span class="se">\&quot;</span><span class="s">export.csv</span><span class="se">\&quot;</span><span class="s">&quot;</span><span class="p" data-group-id="1587397615-4">)</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">send_chunked</span><span class="p" data-group-id="1587397615-5">(</span><span class="ss">:ok</span><span class="p" data-group-id="1587397615-5">)</span><span class="w">

  </span><span class="p" data-group-id="1587397615-6">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="c">_conn</span><span class="p" data-group-id="1587397615-6">}</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">chunk</span><span class="p" data-group-id="1587397615-7">(</span><span class="n">conn</span><span class="p">,</span><span class="w"> </span><span class="nc">CSV</span><span class="o">.</span><span class="n">dump_to_iodata</span><span class="p" data-group-id="1587397615-8">(</span><span class="p" data-group-id="1587397615-9">[</span><span class="p" data-group-id="1587397615-10">[</span><span class="s">&quot;# Sometimes the browser may truncate the file, verify that END OF FILE exists at the bottom&quot;</span><span class="p" data-group-id="1587397615-10">]</span><span class="p" data-group-id="1587397615-9">]</span><span class="p" data-group-id="1587397615-8">)</span><span class="p" data-group-id="1587397615-7">)</span><span class="w">

  </span><span class="c1"># Dump header</span><span class="w">
  </span><span class="n">rows</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">CSV</span><span class="o">.</span><span class="n">dump_to_iodata</span><span class="p" data-group-id="1587397615-11">(</span><span class="p" data-group-id="1587397615-12">[</span><span class="p" data-group-id="1587397615-13">[</span><span class="s">&quot;id&quot;</span><span class="p">,</span><span class="w"> </span><span class="s">&quot;field1&quot;</span><span class="p">,</span><span class="w"> </span><span class="s">&quot;field2&quot;</span><span class="p" data-group-id="1587397615-13">]</span><span class="p" data-group-id="1587397615-12">]</span><span class="p" data-group-id="1587397615-11">)</span><span class="w">
  </span><span class="p" data-group-id="1587397615-14">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="c">_conn</span><span class="p" data-group-id="1587397615-14">}</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">chunk</span><span class="p" data-group-id="1587397615-15">(</span><span class="n">conn</span><span class="p">,</span><span class="w"> </span><span class="n">rows</span><span class="p" data-group-id="1587397615-15">)</span><span class="w">

  </span><span class="c1"># Stream data</span><span class="w">
  </span><span class="nc">Resources</span><span class="o">.</span><span class="n">stream_resources</span><span class="p" data-group-id="1587397615-16">(</span><span class="p" data-group-id="1587397615-16">)</span><span class="w">
  </span><span class="o">|&gt;</span><span class="w"> </span><span class="nc">Stream</span><span class="o">.</span><span class="n">each</span><span class="p" data-group-id="1587397615-17">(</span><span class="k" data-group-id="1587397615-18">fn</span><span class="w"> </span><span class="n">resource</span><span class="w"> </span><span class="o">-&gt;</span><span class="w">
    </span><span class="c1"># Do something potentially expensive here</span><span class="w">
    </span><span class="c1"># row = [id, value1, value2]</span><span class="w">

    </span><span class="p" data-group-id="1587397615-19">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="c">_conn</span><span class="p" data-group-id="1587397615-19">}</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">chunk</span><span class="p" data-group-id="1587397615-20">(</span><span class="n">conn</span><span class="p">,</span><span class="w"> </span><span class="nc">CSV</span><span class="o">.</span><span class="n">dump_to_iodata</span><span class="p" data-group-id="1587397615-21">(</span><span class="p" data-group-id="1587397615-22">[</span><span class="n">row</span><span class="p" data-group-id="1587397615-22">]</span><span class="p" data-group-id="1587397615-21">)</span><span class="p" data-group-id="1587397615-20">)</span><span class="w">
  </span><span class="k" data-group-id="1587397615-18">end</span><span class="p" data-group-id="1587397615-17">)</span><span class="w">
  </span><span class="o">|&gt;</span><span class="w"> </span><span class="nc">Stream</span><span class="o">.</span><span class="n">run</span><span class="p" data-group-id="1587397615-23">(</span><span class="p" data-group-id="1587397615-23">)</span><span class="w">

  </span><span class="p" data-group-id="1587397615-24">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="c">_conn</span><span class="p" data-group-id="1587397615-24">}</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">chunk</span><span class="p" data-group-id="1587397615-25">(</span><span class="n">conn</span><span class="p">,</span><span class="w"> </span><span class="nc">CSV</span><span class="o">.</span><span class="n">dump_to_iodata</span><span class="p" data-group-id="1587397615-26">(</span><span class="p" data-group-id="1587397615-27">[</span><span class="p" data-group-id="1587397615-28">[</span><span class="s">&quot;# END OF FILE&quot;</span><span class="p" data-group-id="1587397615-28">]</span><span class="p" data-group-id="1587397615-27">]</span><span class="p" data-group-id="1587397615-26">)</span><span class="p" data-group-id="1587397615-25">)</span><span class="w">

  </span><span class="n">conn</span><span class="w">
</span><span class="k" data-group-id="1587397615-2">end</span></code></pre>
<p>
The culprit for the streaming being halted was the <code class="inline">idle_timeout</code> setting in <code class="inline">cowboy</code>:</p>
<blockquote>
  <p>
<strong>idle_timeout (60000)</strong>  </p>
  <p>
Time in ms with no data received before Cowboy closes the connection.  </p>
</blockquote>
<p>
We won’t receive any data from the client, though we’re still sending a bunch of data to the client! I don’t know why <code class="inline">cowboy</code> does this as the connection is very much still active with the data being streamed.</p>
<p>
My solution was to switch to <a href="https://github.com/mtrudel/bandit"><code class="inline">Bandit</code></a> which I had already planned. Otherwise, I would have had to find a way to get rid of <code class="inline">idle_timeout</code> for the CSV export, which I’m not even sure is possible with <code class="inline">cowboy</code>.</p>
<h2>
Connection halt</h2>
<p>
A small detail in the above logic is that we only handle happy paths. If the end-user stops the download midway we will experience a match error, as <a href="https://hexdocs.pm/plug/Plug.Conn.html#chunk/2"><code class="inline">Plug.Conn.chunk/2</code></a> will return <code class="inline">{:error, :closed}</code>. If we don’t want to have the process blow up, we should handle it by halting the stream (i.e. returning <code class="inline">{:halt, acc}</code> using <code class="inline">Stream.transform/3</code>). For our particular use case, it was fine that it blew up.</p>
<h2>
Addendum: Sequence data in Postgres</h2>
<p>
A small addendum to this was how I tested this in our staging environment. With our staging environment, we have no shell access so I couldn’t use the IEx console to insert a large amount of data.</p>
<p>
I did have access to Postgres though so I ran a query with <code class="inline">generate_series</code>:</p>
<pre><code class="sql">INSERT INTO responses(id, field1, field2, inserted_at, updated_at)
SELECT
  gen_random_uuid(),
  &#39;value1-&#39; || a.n,
  &#39;email-&#39; || a.n || &#39;@example.com&#39;,
  &#39;2024-01-12 03:09:53.549195&#39;::timestamp + (a.n || &#39; ms&#39;)::interval,
  &#39;2024-01-12 03:09:53.549195&#39;::timestamp + (a.n || &#39; ms&#39;)::interval
FROM generate_series(1, 100000) as a(n)</code></pre>
<h2>
Future</h2>
<p>
In the above, we’re streaming the data directly to the end-user. The proper way would instead be to push this to a background task, storing the CSV file somewhere where it can be served as static content (e.g. S3). Streaming data directly could become expensive, error-prone, and potentially a vulnerability.</p>
<p>
We may also want to speed up the CSV export by parallelizing transformations of rows (iterate through chunks of rows instead of each row).</p>

      ]]>
    </content>
  </entry>
  
  <entry>
    <title>Search DSL using NimbleParsec</title>
    <link href="https://danschultzer.com/posts/search-dsl-using-nimbleparsec" />
    <id>https://danschultzer.com/posts/search-dsl-using-nimbleparsec</id>
    <updated>2023-12-17T00:00:00Z</updated>
    <summary>Using NimbleParsec to build a search DSL.</summary>
    <content type="html">
      <![CDATA[
        <p>
I have wanted to use <a href="https://hex.pm/packages/nimble_parsec"><code class="inline">NimbleParsec</code></a> for a while, and finally got a good use case when I had to implement a search DSL in a project.</p>
<p>
The requirement was to parse a query string into <a href="https://hex.pm/packages/flop"><code class="inline">Flop</code></a> filters. The search query would look like this:</p>
<pre><code>inserted_at&gt;=1702744783 AND amount&lt;10</code></pre>
<p>
As this was implemented in an API, we will start at the controller level by setting up this plug function:</p>
<pre><code class="makeup elixir"><span class="kd">defp</span><span class="w"> </span><span class="nf">parse_flop_filters</span><span class="p" data-group-id="8146329898-1">(</span><span class="p" data-group-id="8146329898-2">%{</span><span class="ss">params</span><span class="p">:</span><span class="w"> </span><span class="p" data-group-id="8146329898-3">%{</span><span class="s">&quot;query&quot;</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="n">query</span><span class="p" data-group-id="8146329898-3">}</span><span class="p" data-group-id="8146329898-2">}</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">conn</span><span class="p">,</span><span class="w"> </span><span class="c">_opts</span><span class="p" data-group-id="8146329898-1">)</span><span class="w"> </span><span class="k" data-group-id="8146329898-4">do</span><span class="w">
  </span><span class="k">case</span><span class="w"> </span><span class="nc">QueryParser</span><span class="o">.</span><span class="n">parse</span><span class="p" data-group-id="8146329898-5">(</span><span class="n">query</span><span class="p">,</span><span class="w"> </span><span class="ss">inserted_at</span><span class="p">:</span><span class="w"> </span><span class="ss">:utc_datetime</span><span class="p">,</span><span class="w"> </span><span class="ss">amount</span><span class="p">:</span><span class="w"> </span><span class="ss">:integer</span><span class="p" data-group-id="8146329898-5">)</span><span class="w"> </span><span class="k" data-group-id="8146329898-6">do</span><span class="w">
    </span><span class="p" data-group-id="8146329898-7">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="n">filters</span><span class="p" data-group-id="8146329898-7">}</span><span class="w"> </span><span class="o">-&gt;</span><span class="w">
      </span><span class="n">assign</span><span class="p" data-group-id="8146329898-8">(</span><span class="n">conn</span><span class="p">,</span><span class="w"> </span><span class="ss">:flop_options</span><span class="p">,</span><span class="w"> </span><span class="nc">Map</span><span class="o">.</span><span class="n">put</span><span class="p" data-group-id="8146329898-9">(</span><span class="n">conn</span><span class="o">.</span><span class="n">assigns</span><span class="o">.</span><span class="n">flop_options</span><span class="p">,</span><span class="w"> </span><span class="ss">:filters</span><span class="p">,</span><span class="w"> </span><span class="n">filters</span><span class="p" data-group-id="8146329898-9">)</span><span class="p" data-group-id="8146329898-8">)</span><span class="w">

    </span><span class="p" data-group-id="8146329898-10">{</span><span class="ss">:error</span><span class="p">,</span><span class="w"> </span><span class="n">error</span><span class="p" data-group-id="8146329898-10">}</span><span class="w"> </span><span class="o">-&gt;</span><span class="w">
      </span><span class="n">conn</span><span class="w">
      </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">put_status</span><span class="p" data-group-id="8146329898-11">(</span><span class="mi">422</span><span class="p" data-group-id="8146329898-11">)</span><span class="w">
      </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">json</span><span class="p" data-group-id="8146329898-12">(</span><span class="p" data-group-id="8146329898-13">%{</span><span class="ss">error</span><span class="p">:</span><span class="w"> </span><span class="s">&quot;Invalid query: </span><span class="si" data-group-id="8146329898-14">#{</span><span class="n">error</span><span class="si" data-group-id="8146329898-14">}</span><span class="s">&quot;</span><span class="p" data-group-id="8146329898-13">}</span><span class="p" data-group-id="8146329898-12">)</span><span class="w">
      </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">halt</span><span class="p" data-group-id="8146329898-15">(</span><span class="p" data-group-id="8146329898-15">)</span><span class="w">
  </span><span class="k" data-group-id="8146329898-6">end</span><span class="w">
</span><span class="k" data-group-id="8146329898-4">end</span></code></pre>
<p>
We should be explicit here, only allowing fields we want to handle in the API rather than passing everything on to <code class="inline">Flop</code>. In the above, we’re passing in the search fields and their types. The type is necessary because we want to parse from unix epoch timestamps (which we use in the API) to <code class="inline">DateTime</code> that <code class="inline">Flop</code> expects.</p>
<p>
Now we’ll implement our <code class="inline">parse/2</code> function:</p>
<pre><code class="makeup elixir"><span class="kd">defmodule</span><span class="w"> </span><span class="nc">MyAppWeb.QueryParser</span><span class="w"> </span><span class="k" data-group-id="6464373316-1">do</span><span class="w">
  </span><span class="kn">import</span><span class="w"> </span><span class="nc">NimbleParsec</span><span class="w">
  </span><span class="kn">import</span><span class="w"> </span><span class="bp">__MODULE__</span><span class="o">.</span><span class="nc">Helpers</span><span class="w">

  </span><span class="n">defparsec</span><span class="w"> </span><span class="ss">:do_parse</span><span class="p">,</span><span class="w"> </span><span class="n">query</span><span class="p" data-group-id="6464373316-2">(</span><span class="p" data-group-id="6464373316-2">)</span><span class="w">

  </span><span class="kd">def</span><span class="w"> </span><span class="nf">parse</span><span class="p" data-group-id="6464373316-3">(</span><span class="n">query</span><span class="p">,</span><span class="w"> </span><span class="n">query_fields</span><span class="p" data-group-id="6464373316-3">)</span><span class="w"> </span><span class="k" data-group-id="6464373316-4">do</span><span class="w">
    </span><span class="k">with</span><span class="w"> </span><span class="p" data-group-id="6464373316-5">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="n">filters</span><span class="p">,</span><span class="w"> </span><span class="s">&quot;&quot;</span><span class="p">,</span><span class="w"> </span><span class="bp">_</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="6464373316-6">{</span><span class="bp">_</span><span class="p">,</span><span class="w"> </span><span class="bp">_</span><span class="p" data-group-id="6464373316-6">}</span><span class="p">,</span><span class="w"> </span><span class="bp">_</span><span class="p" data-group-id="6464373316-5">}</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="n">do_parse</span><span class="p" data-group-id="6464373316-7">(</span><span class="n">query</span><span class="p" data-group-id="6464373316-7">)</span><span class="p">,</span><span class="w">
         </span><span class="ss">:ok</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="n">validate_fields</span><span class="p" data-group-id="6464373316-8">(</span><span class="n">filters</span><span class="p">,</span><span class="w"> </span><span class="n">query_fields</span><span class="p" data-group-id="6464373316-8">)</span><span class="p">,</span><span class="w">
         </span><span class="p" data-group-id="6464373316-9">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="n">filters</span><span class="p" data-group-id="6464373316-9">}</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="n">parse_timestamps</span><span class="p" data-group-id="6464373316-10">(</span><span class="n">filters</span><span class="p">,</span><span class="w"> </span><span class="n">query_fields</span><span class="p" data-group-id="6464373316-10">)</span><span class="w"> </span><span class="k" data-group-id="6464373316-11">do</span><span class="w">
      </span><span class="p" data-group-id="6464373316-12">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="n">filters</span><span class="p" data-group-id="6464373316-12">}</span><span class="w">
    </span><span class="k" data-group-id="6464373316-11">else</span><span class="w">
      </span><span class="p" data-group-id="6464373316-13">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="c">_filters</span><span class="p">,</span><span class="w"> </span><span class="n">string</span><span class="p">,</span><span class="w"> </span><span class="bp">_</span><span class="p">,</span><span class="w"> </span><span class="bp">_</span><span class="p">,</span><span class="w"> </span><span class="bp">_</span><span class="p" data-group-id="6464373316-13">}</span><span class="w"> </span><span class="o">-&gt;</span><span class="w"> </span><span class="p" data-group-id="6464373316-14">{</span><span class="ss">:error</span><span class="p">,</span><span class="w"> </span><span class="s">&quot;couldn&#39;t process all of the query, got </span><span class="si" data-group-id="6464373316-15">#{</span><span class="n">inspect</span><span class="w"> </span><span class="n">string</span><span class="si" data-group-id="6464373316-15">}</span><span class="s">&quot;</span><span class="p" data-group-id="6464373316-14">}</span><span class="w">
      </span><span class="p" data-group-id="6464373316-16">{</span><span class="ss">:error</span><span class="p">,</span><span class="w"> </span><span class="n">error</span><span class="p">,</span><span class="w"> </span><span class="bp">_</span><span class="p">,</span><span class="w"> </span><span class="bp">_</span><span class="p">,</span><span class="w"> </span><span class="bp">_</span><span class="p">,</span><span class="w"> </span><span class="bp">_</span><span class="p" data-group-id="6464373316-16">}</span><span class="w"> </span><span class="o">-&gt;</span><span class="w"> </span><span class="p" data-group-id="6464373316-17">{</span><span class="ss">:error</span><span class="p">,</span><span class="w"> </span><span class="n">error</span><span class="p" data-group-id="6464373316-17">}</span><span class="w">
      </span><span class="p" data-group-id="6464373316-18">{</span><span class="ss">:error</span><span class="p">,</span><span class="w"> </span><span class="n">error</span><span class="p" data-group-id="6464373316-18">}</span><span class="w"> </span><span class="o">-&gt;</span><span class="w"> </span><span class="p" data-group-id="6464373316-19">{</span><span class="ss">:error</span><span class="p">,</span><span class="w"> </span><span class="n">error</span><span class="p" data-group-id="6464373316-19">}</span><span class="w">
    </span><span class="k" data-group-id="6464373316-11">end</span><span class="w">
  </span><span class="k" data-group-id="6464373316-4">end</span><span class="w">

  </span><span class="kd">defp</span><span class="w"> </span><span class="nf">validate_fields</span><span class="p" data-group-id="6464373316-20">(</span><span class="n">filters</span><span class="p">,</span><span class="w"> </span><span class="n">query_fields</span><span class="p" data-group-id="6464373316-20">)</span><span class="w"> </span><span class="k" data-group-id="6464373316-21">do</span><span class="w">
    </span><span class="n">expected_fields</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">Enum</span><span class="o">.</span><span class="n">map</span><span class="p" data-group-id="6464373316-22">(</span><span class="n">query_fields</span><span class="p">,</span><span class="w"> </span><span class="k" data-group-id="6464373316-23">fn</span><span class="w"> </span><span class="p" data-group-id="6464373316-24">{</span><span class="n">key</span><span class="p">,</span><span class="w"> </span><span class="c">_type</span><span class="p" data-group-id="6464373316-24">}</span><span class="w"> </span><span class="o">-&gt;</span><span class="w"> </span><span class="n">to_string</span><span class="p" data-group-id="6464373316-25">(</span><span class="n">key</span><span class="p" data-group-id="6464373316-25">)</span><span class="w"> </span><span class="k" data-group-id="6464373316-23">end</span><span class="p" data-group-id="6464373316-22">)</span><span class="w">
    </span><span class="n">current_fields</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">Enum</span><span class="o">.</span><span class="n">map</span><span class="p" data-group-id="6464373316-26">(</span><span class="n">filters</span><span class="p">,</span><span class="w"> </span><span class="o">&amp;</span><span class="w"> </span><span class="ni">&amp;1</span><span class="o">.</span><span class="n">field</span><span class="p" data-group-id="6464373316-26">)</span><span class="w">

    </span><span class="k">case</span><span class="w"> </span><span class="n">current_fields</span><span class="w"> </span><span class="o">--</span><span class="w"> </span><span class="n">expected_fields</span><span class="w"> </span><span class="k" data-group-id="6464373316-27">do</span><span class="w">
      </span><span class="p" data-group-id="6464373316-28">[</span><span class="p" data-group-id="6464373316-28">]</span><span class="w"> </span><span class="o">-&gt;</span><span class="w"> </span><span class="ss">:ok</span><span class="w">
      </span><span class="n">unexpected_fields</span><span class="w"> </span><span class="o">-&gt;</span><span class="w"> </span><span class="p" data-group-id="6464373316-29">{</span><span class="ss">:error</span><span class="p">,</span><span class="w"> </span><span class="s">&quot;unknown filter fields, got </span><span class="si" data-group-id="6464373316-30">#{</span><span class="nc">Enum</span><span class="o">.</span><span class="n">join</span><span class="p" data-group-id="6464373316-31">(</span><span class="n">unexpected_fields</span><span class="p">,</span><span class="w"> </span><span class="s">&quot;, &quot;</span><span class="p" data-group-id="6464373316-31">)</span><span class="si" data-group-id="6464373316-30">}</span><span class="s">&quot;</span><span class="p" data-group-id="6464373316-29">}</span><span class="w">
    </span><span class="k" data-group-id="6464373316-27">end</span><span class="w">
  </span><span class="k" data-group-id="6464373316-21">end</span><span class="w">

  </span><span class="c1"># ...</span><span class="w">
</span><span class="k" data-group-id="6464373316-1">end</span></code></pre>
<p>
In the above, we will first parse the query string with <code class="inline">NimbleParsec</code>, and then validate the search fields. Finally, we are doing post-processing where we cast the unix timestamps into <code class="inline">DateTime</code>. The validation and casting of the timestamp looks like this:</p>
<pre><code class="makeup elixir"><span class="kd">defmodule</span><span class="w"> </span><span class="nc">MyAppWeb.QueryParser</span><span class="w"> </span><span class="k" data-group-id="6026165621-1">do</span><span class="w">
  </span><span class="c1"># ...</span><span class="w">

  </span><span class="kd">defp</span><span class="w"> </span><span class="nf">parse_timestamps</span><span class="p" data-group-id="6026165621-2">(</span><span class="n">filters</span><span class="p">,</span><span class="w"> </span><span class="n">query_fields</span><span class="p" data-group-id="6026165621-2">)</span><span class="w"> </span><span class="k" data-group-id="6026165621-3">do</span><span class="w">
    </span><span class="n">fields</span><span class="w"> </span><span class="o">=</span><span class="w">
      </span><span class="n">query_fields</span><span class="w">
      </span><span class="o">|&gt;</span><span class="w"> </span><span class="nc">Enum</span><span class="o">.</span><span class="n">filter</span><span class="p" data-group-id="6026165621-4">(</span><span class="k" data-group-id="6026165621-5">fn</span><span class="w"> </span><span class="p" data-group-id="6026165621-6">{</span><span class="c">_field</span><span class="p">,</span><span class="w"> </span><span class="n">type</span><span class="p" data-group-id="6026165621-6">}</span><span class="w"> </span><span class="o">-&gt;</span><span class="w"> </span><span class="n">type</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="ss">:utc_datetime</span><span class="w"> </span><span class="k" data-group-id="6026165621-5">end</span><span class="p" data-group-id="6026165621-4">)</span><span class="w">
      </span><span class="o">|&gt;</span><span class="w"> </span><span class="nc">Enum</span><span class="o">.</span><span class="n">map</span><span class="p" data-group-id="6026165621-7">(</span><span class="k" data-group-id="6026165621-8">fn</span><span class="w"> </span><span class="p" data-group-id="6026165621-9">{</span><span class="n">field</span><span class="p">,</span><span class="w"> </span><span class="c">_type</span><span class="p" data-group-id="6026165621-9">}</span><span class="w"> </span><span class="o">-&gt;</span><span class="w"> </span><span class="n">to_string</span><span class="p" data-group-id="6026165621-10">(</span><span class="n">field</span><span class="p" data-group-id="6026165621-10">)</span><span class="w"> </span><span class="k" data-group-id="6026165621-8">end</span><span class="p" data-group-id="6026165621-7">)</span><span class="w">

    </span><span class="nc">Enum</span><span class="o">.</span><span class="n">reduce_while</span><span class="p" data-group-id="6026165621-11">(</span><span class="n">filters</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="6026165621-12">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="6026165621-13">[</span><span class="p" data-group-id="6026165621-13">]</span><span class="p" data-group-id="6026165621-12">}</span><span class="p">,</span><span class="w"> </span><span class="k" data-group-id="6026165621-14">fn</span><span class="w"> </span><span class="n">filter</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="6026165621-15">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="n">filters</span><span class="p" data-group-id="6026165621-15">}</span><span class="w"> </span><span class="o">-&gt;</span><span class="w">
      </span><span class="k">case</span><span class="w"> </span><span class="n">update_timestamp</span><span class="p" data-group-id="6026165621-16">(</span><span class="n">filter</span><span class="p">,</span><span class="w"> </span><span class="n">fields</span><span class="p" data-group-id="6026165621-16">)</span><span class="w"> </span><span class="k" data-group-id="6026165621-17">do</span><span class="w">
        </span><span class="p" data-group-id="6026165621-18">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="n">filter</span><span class="p" data-group-id="6026165621-18">}</span><span class="w"> </span><span class="o">-&gt;</span><span class="w"> </span><span class="p" data-group-id="6026165621-19">{</span><span class="ss">:cont</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="6026165621-20">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="n">filters</span><span class="w"> </span><span class="o">++</span><span class="w"> </span><span class="p" data-group-id="6026165621-21">[</span><span class="n">filter</span><span class="p" data-group-id="6026165621-21">]</span><span class="p" data-group-id="6026165621-20">}</span><span class="p" data-group-id="6026165621-19">}</span><span class="w">
        </span><span class="p" data-group-id="6026165621-22">{</span><span class="ss">:error</span><span class="p">,</span><span class="w"> </span><span class="n">error</span><span class="p" data-group-id="6026165621-22">}</span><span class="w"> </span><span class="o">-&gt;</span><span class="w"> </span><span class="p" data-group-id="6026165621-23">{</span><span class="ss">:halt</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="6026165621-24">{</span><span class="ss">:error</span><span class="p">,</span><span class="w"> </span><span class="n">error</span><span class="p" data-group-id="6026165621-24">}</span><span class="p" data-group-id="6026165621-23">}</span><span class="w">
      </span><span class="k" data-group-id="6026165621-17">end</span><span class="w">
    </span><span class="k" data-group-id="6026165621-14">end</span><span class="p" data-group-id="6026165621-11">)</span><span class="w">
  </span><span class="k" data-group-id="6026165621-3">end</span><span class="w">

  </span><span class="kd">defp</span><span class="w"> </span><span class="nf">update_timestamp</span><span class="p" data-group-id="6026165621-25">(</span><span class="p" data-group-id="6026165621-26">%{</span><span class="ss">field</span><span class="p">:</span><span class="w"> </span><span class="n">field</span><span class="p">,</span><span class="w"> </span><span class="ss">value</span><span class="p">:</span><span class="w"> </span><span class="n">value</span><span class="p" data-group-id="6026165621-26">}</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">filter</span><span class="p">,</span><span class="w"> </span><span class="n">fields</span><span class="p" data-group-id="6026165621-25">)</span><span class="w"> </span><span class="k" data-group-id="6026165621-27">do</span><span class="w">
    </span><span class="k">case</span><span class="w"> </span><span class="nc">Enum</span><span class="o">.</span><span class="n">member?</span><span class="p" data-group-id="6026165621-28">(</span><span class="n">fields</span><span class="p">,</span><span class="w"> </span><span class="n">field</span><span class="p" data-group-id="6026165621-28">)</span><span class="w"> </span><span class="k" data-group-id="6026165621-29">do</span><span class="w">
      </span><span class="no">true</span><span class="w"> </span><span class="o">-&gt;</span><span class="w">
        </span><span class="k">case</span><span class="w"> </span><span class="n">from_unix_epoch</span><span class="p" data-group-id="6026165621-30">(</span><span class="n">value</span><span class="p" data-group-id="6026165621-30">)</span><span class="w"> </span><span class="k" data-group-id="6026165621-31">do</span><span class="w">
          </span><span class="p" data-group-id="6026165621-32">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="n">datetime</span><span class="p" data-group-id="6026165621-32">}</span><span class="w"> </span><span class="o">-&gt;</span><span class="w"> </span><span class="p" data-group-id="6026165621-33">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="6026165621-34">%{</span><span class="n">filter</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="ss">value</span><span class="p">:</span><span class="w"> </span><span class="n">datetime</span><span class="p" data-group-id="6026165621-34">}</span><span class="p" data-group-id="6026165621-33">}</span><span class="w">
          </span><span class="c">_error</span><span class="w"> </span><span class="o">-&gt;</span><span class="w"> </span><span class="p" data-group-id="6026165621-35">{</span><span class="ss">:error</span><span class="p">,</span><span class="w"> </span><span class="s">&quot;unexpected timestamp value for </span><span class="si" data-group-id="6026165621-36">#{</span><span class="n">field</span><span class="si" data-group-id="6026165621-36">}</span><span class="s">, got </span><span class="si" data-group-id="6026165621-37">#{</span><span class="n">value</span><span class="si" data-group-id="6026165621-37">}</span><span class="s">&quot;</span><span class="p" data-group-id="6026165621-35">}</span><span class="w">
        </span><span class="k" data-group-id="6026165621-31">end</span><span class="w">

      </span><span class="no">false</span><span class="w"> </span><span class="o">-&gt;</span><span class="w">
        </span><span class="p" data-group-id="6026165621-38">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="n">filter</span><span class="p" data-group-id="6026165621-38">}</span><span class="w">
    </span><span class="k" data-group-id="6026165621-29">end</span><span class="w">
  </span><span class="k" data-group-id="6026165621-27">end</span><span class="w">

  </span><span class="kd">defp</span><span class="w"> </span><span class="nf">from_unix_epoch</span><span class="p" data-group-id="6026165621-39">(</span><span class="n">value</span><span class="p" data-group-id="6026165621-39">)</span><span class="w"> </span><span class="ow">when</span><span class="w"> </span><span class="n">is_integer</span><span class="p" data-group-id="6026165621-40">(</span><span class="n">value</span><span class="p" data-group-id="6026165621-40">)</span><span class="w"> </span><span class="k" data-group-id="6026165621-41">do</span><span class="w">
    </span><span class="nc">DateTime</span><span class="o">.</span><span class="n">from_unix</span><span class="p" data-group-id="6026165621-42">(</span><span class="n">value</span><span class="p" data-group-id="6026165621-42">)</span><span class="w">
  </span><span class="k" data-group-id="6026165621-41">end</span><span class="w">

  </span><span class="kd">defp</span><span class="w"> </span><span class="nf">from_unix_epoch</span><span class="p" data-group-id="6026165621-43">(</span><span class="c">_value</span><span class="p" data-group-id="6026165621-43">)</span><span class="p">,</span><span class="w"> </span><span class="ss">do</span><span class="p">:</span><span class="w"> </span><span class="ss">:error</span><span class="w">
</span><span class="k" data-group-id="6026165621-1">end</span></code></pre>
<p>
Now all that’s left to do is implement the parser itself! I highly recommend checking out <a href="https://github.com/Logflare/logflare/blob/v1.5.12/lib/logflare/logs/lql/lql_parser_helpers.ex">Logflare’s parser</a> which gave me what I needed to set this up.</p>
<p>
We begin by setting up the <code class="inline">query</code> combinator:</p>
<pre><code class="makeup elixir"><span class="kd">defmodule</span><span class="w"> </span><span class="nc">MyAppWeb.QueryParser.Helpers</span><span class="w"> </span><span class="k" data-group-id="9294624911-1">do</span><span class="w">
  </span><span class="kn">import</span><span class="w"> </span><span class="nc">NimbleParsec</span><span class="w">

  </span><span class="kd">def</span><span class="w"> </span><span class="nf">query</span><span class="w"> </span><span class="k" data-group-id="9294624911-2">do</span><span class="w">
    </span><span class="n">field_clause</span><span class="p" data-group-id="9294624911-3">(</span><span class="p" data-group-id="9294624911-3">)</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">optional</span><span class="p" data-group-id="9294624911-4">(</span><span class="w">
      </span><span class="n">ignore</span><span class="p" data-group-id="9294624911-5">(</span><span class="n">whitespace</span><span class="p" data-group-id="9294624911-6">(</span><span class="p" data-group-id="9294624911-6">)</span><span class="p" data-group-id="9294624911-5">)</span><span class="w">
      </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">concat</span><span class="p" data-group-id="9294624911-7">(</span><span class="n">query_operator</span><span class="p" data-group-id="9294624911-8">(</span><span class="p" data-group-id="9294624911-8">)</span><span class="p" data-group-id="9294624911-7">)</span><span class="w">
      </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">concat</span><span class="p" data-group-id="9294624911-9">(</span><span class="n">ignore</span><span class="p" data-group-id="9294624911-10">(</span><span class="n">whitespace</span><span class="p" data-group-id="9294624911-11">(</span><span class="p" data-group-id="9294624911-11">)</span><span class="p" data-group-id="9294624911-10">)</span><span class="p" data-group-id="9294624911-9">)</span><span class="p" data-group-id="9294624911-4">)</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">times</span><span class="p" data-group-id="9294624911-12">(</span><span class="ss">min</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span><span class="w"> </span><span class="ss">max</span><span class="p">:</span><span class="w"> </span><span class="mi">100</span><span class="p" data-group-id="9294624911-12">)</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">post_traverse</span><span class="p" data-group-id="9294624911-13">(</span><span class="p" data-group-id="9294624911-14">{</span><span class="ss">:to_filters</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="9294624911-15">[</span><span class="p" data-group-id="9294624911-15">]</span><span class="p" data-group-id="9294624911-14">}</span><span class="p" data-group-id="9294624911-13">)</span><span class="w">
  </span><span class="k" data-group-id="9294624911-2">end</span><span class="w">

  </span><span class="c1"># ...</span><span class="w">

  </span><span class="kd">def</span><span class="w"> </span><span class="nf">to_filters</span><span class="p" data-group-id="9294624911-16">(</span><span class="n">rest</span><span class="p">,</span><span class="w"> </span><span class="n">args</span><span class="p">,</span><span class="w"> </span><span class="n">context</span><span class="p">,</span><span class="w"> </span><span class="c">_line</span><span class="p">,</span><span class="w"> </span><span class="c">_offset</span><span class="p" data-group-id="9294624911-16">)</span><span class="w"> </span><span class="k" data-group-id="9294624911-17">do</span><span class="w">
    </span><span class="n">filters</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">for</span><span class="w"> </span><span class="p" data-group-id="9294624911-18">%{</span><span class="ss">field</span><span class="p">:</span><span class="w"> </span><span class="bp">_</span><span class="p" data-group-id="9294624911-18">}</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">filter</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="n">args</span><span class="p">,</span><span class="w"> </span><span class="ss">do</span><span class="p">:</span><span class="w"> </span><span class="n">filter</span><span class="w">
    </span><span class="p" data-group-id="9294624911-19">{</span><span class="n">rest</span><span class="p">,</span><span class="w"> </span><span class="n">filters</span><span class="p">,</span><span class="w"> </span><span class="n">context</span><span class="p" data-group-id="9294624911-19">}</span><span class="w">
  </span><span class="k" data-group-id="9294624911-17">end</span><span class="w">
</span><span class="k" data-group-id="9294624911-1">end</span></code></pre>
<p>
The <code class="inline">query</code> combinator matches field clauses with an optional query operator following it. The post traverse was necessary because I couldn’t just use <code class="inline">ignore</code> with the query operator since I will verify it’s a valid operator (as you’ll see later). So instead I had to let it match and filter it out at the end.</p>
<p>
Now we’ll implement the field clause combinator:</p>
<pre><code class="makeup elixir"><span class="kd">defmodule</span><span class="w"> </span><span class="nc">MyAppWeb.QueryParser.Helpers</span><span class="w"> </span><span class="k" data-group-id="5836966134-1">do</span><span class="w">
  </span><span class="c1"># ...</span><span class="w">

  </span><span class="kd">def</span><span class="w"> </span><span class="nf">field_clause</span><span class="w"> </span><span class="k" data-group-id="5836966134-2">do</span><span class="w">
    </span><span class="n">any_field</span><span class="p" data-group-id="5836966134-3">(</span><span class="p" data-group-id="5836966134-3">)</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">concat</span><span class="p" data-group-id="5836966134-4">(</span><span class="n">operator</span><span class="p" data-group-id="5836966134-5">(</span><span class="p" data-group-id="5836966134-5">)</span><span class="p" data-group-id="5836966134-4">)</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">concat</span><span class="p" data-group-id="5836966134-6">(</span><span class="n">field_value</span><span class="p" data-group-id="5836966134-7">(</span><span class="p" data-group-id="5836966134-7">)</span><span class="p" data-group-id="5836966134-6">)</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">reduce</span><span class="p" data-group-id="5836966134-8">(</span><span class="p" data-group-id="5836966134-9">{</span><span class="ss">:to_filter</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="5836966134-10">[</span><span class="p" data-group-id="5836966134-10">]</span><span class="p" data-group-id="5836966134-9">}</span><span class="p" data-group-id="5836966134-8">)</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">map</span><span class="p" data-group-id="5836966134-11">(</span><span class="p" data-group-id="5836966134-12">{</span><span class="ss">:check_for_no_invalid_field_values!</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="5836966134-13">[</span><span class="p" data-group-id="5836966134-13">]</span><span class="p" data-group-id="5836966134-12">}</span><span class="p" data-group-id="5836966134-11">)</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">label</span><span class="p" data-group-id="5836966134-14">(</span><span class="s">&quot;field filter clause&quot;</span><span class="p" data-group-id="5836966134-14">)</span><span class="w">
  </span><span class="k" data-group-id="5836966134-2">end</span><span class="w">

  </span><span class="kd">def</span><span class="w"> </span><span class="nf">any_field</span><span class="w"> </span><span class="k" data-group-id="5836966134-15">do</span><span class="w">
    </span><span class="n">ascii_string</span><span class="p" data-group-id="5836966134-16">(</span><span class="p" data-group-id="5836966134-17">[</span><span class="sc">?a</span><span class="o">..</span><span class="sc">?z</span><span class="p">,</span><span class="w"> </span><span class="sc">?A</span><span class="o">..</span><span class="sc">?Z</span><span class="p">,</span><span class="w"> </span><span class="sc">?_</span><span class="p" data-group-id="5836966134-17">]</span><span class="p">,</span><span class="w"> </span><span class="ss">min</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="p" data-group-id="5836966134-16">)</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">reduce</span><span class="p" data-group-id="5836966134-18">(</span><span class="p" data-group-id="5836966134-19">{</span><span class="nc">List</span><span class="p">,</span><span class="w"> </span><span class="ss">:to_string</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="5836966134-20">[</span><span class="p" data-group-id="5836966134-20">]</span><span class="p" data-group-id="5836966134-19">}</span><span class="p" data-group-id="5836966134-18">)</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">unwrap_and_tag</span><span class="p" data-group-id="5836966134-21">(</span><span class="ss">:field</span><span class="p" data-group-id="5836966134-21">)</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">label</span><span class="p" data-group-id="5836966134-22">(</span><span class="s">&quot;filter field&quot;</span><span class="p" data-group-id="5836966134-22">)</span><span class="w">
  </span><span class="k" data-group-id="5836966134-15">end</span><span class="w">

  </span><span class="kd">def</span><span class="w"> </span><span class="nf">operator</span><span class="w"> </span><span class="k" data-group-id="5836966134-23">do</span><span class="w">
    </span><span class="n">choice</span><span class="p" data-group-id="5836966134-24">(</span><span class="p" data-group-id="5836966134-25">[</span><span class="w">
      </span><span class="n">string</span><span class="p" data-group-id="5836966134-26">(</span><span class="s">&quot;&gt;=&quot;</span><span class="p" data-group-id="5836966134-26">)</span><span class="w"> </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">replace</span><span class="p" data-group-id="5836966134-27">(</span><span class="ss">:&gt;=</span><span class="p" data-group-id="5836966134-27">)</span><span class="p">,</span><span class="w">
      </span><span class="n">string</span><span class="p" data-group-id="5836966134-28">(</span><span class="s">&quot;&lt;=&quot;</span><span class="p" data-group-id="5836966134-28">)</span><span class="w"> </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">replace</span><span class="p" data-group-id="5836966134-29">(</span><span class="ss">:&lt;=</span><span class="p" data-group-id="5836966134-29">)</span><span class="p">,</span><span class="w">
      </span><span class="n">string</span><span class="p" data-group-id="5836966134-30">(</span><span class="s">&quot;&gt;&quot;</span><span class="p" data-group-id="5836966134-30">)</span><span class="w"> </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">replace</span><span class="p" data-group-id="5836966134-31">(</span><span class="ss">:&gt;</span><span class="p" data-group-id="5836966134-31">)</span><span class="p">,</span><span class="w">
      </span><span class="n">string</span><span class="p" data-group-id="5836966134-32">(</span><span class="s">&quot;&lt;&quot;</span><span class="p" data-group-id="5836966134-32">)</span><span class="w"> </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">replace</span><span class="p" data-group-id="5836966134-33">(</span><span class="ss">:&lt;</span><span class="p" data-group-id="5836966134-33">)</span><span class="p">,</span><span class="w">
    </span><span class="p" data-group-id="5836966134-25">]</span><span class="p" data-group-id="5836966134-24">)</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">unwrap_and_tag</span><span class="p" data-group-id="5836966134-34">(</span><span class="ss">:operator</span><span class="p" data-group-id="5836966134-34">)</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">label</span><span class="p" data-group-id="5836966134-35">(</span><span class="s">&quot;filter operator&quot;</span><span class="p" data-group-id="5836966134-35">)</span><span class="w">
  </span><span class="k" data-group-id="5836966134-23">end</span><span class="w">

  </span><span class="kd">def</span><span class="w"> </span><span class="nf">field_value</span><span class="w"> </span><span class="k" data-group-id="5836966134-36">do</span><span class="w">
    </span><span class="n">choice</span><span class="p" data-group-id="5836966134-37">(</span><span class="p" data-group-id="5836966134-38">[</span><span class="w">
      </span><span class="n">number</span><span class="p" data-group-id="5836966134-39">(</span><span class="p" data-group-id="5836966134-39">)</span><span class="p">,</span><span class="w">
      </span><span class="n">ascii_string</span><span class="p" data-group-id="5836966134-40">(</span><span class="p" data-group-id="5836966134-41">[</span><span class="sc">?a</span><span class="o">..</span><span class="sc">?z</span><span class="p">,</span><span class="w"> </span><span class="sc">?A</span><span class="o">..</span><span class="sc">?Z</span><span class="p">,</span><span class="w"> </span><span class="sc">?_</span><span class="p">,</span><span class="w"> </span><span class="sc">?0</span><span class="o">..</span><span class="sc">?9</span><span class="p" data-group-id="5836966134-41">]</span><span class="p">,</span><span class="w"> </span><span class="ss">min</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="p" data-group-id="5836966134-40">)</span><span class="p">,</span><span class="w">
      </span><span class="n">invalid_match_all_value</span><span class="p" data-group-id="5836966134-42">(</span><span class="p" data-group-id="5836966134-42">)</span><span class="w">
    </span><span class="p" data-group-id="5836966134-38">]</span><span class="p" data-group-id="5836966134-37">)</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">unwrap_and_tag</span><span class="p" data-group-id="5836966134-43">(</span><span class="ss">:value</span><span class="p" data-group-id="5836966134-43">)</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">label</span><span class="p" data-group-id="5836966134-44">(</span><span class="s">&quot;valid filter value&quot;</span><span class="p" data-group-id="5836966134-44">)</span><span class="w">
  </span><span class="k" data-group-id="5836966134-36">end</span><span class="w">

  </span><span class="kd">def</span><span class="w"> </span><span class="nf">number</span><span class="w"> </span><span class="k" data-group-id="5836966134-45">do</span><span class="w">
    </span><span class="n">integer</span><span class="p" data-group-id="5836966134-46">(</span><span class="ss">min</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="p" data-group-id="5836966134-46">)</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">label</span><span class="p" data-group-id="5836966134-47">(</span><span class="s">&quot;number&quot;</span><span class="p" data-group-id="5836966134-47">)</span><span class="w">
  </span><span class="k" data-group-id="5836966134-45">end</span><span class="w">

  </span><span class="kd">def</span><span class="w"> </span><span class="nf">invalid_match_all_value</span><span class="w"> </span><span class="k" data-group-id="5836966134-48">do</span><span class="w">
    </span><span class="n">choice</span><span class="p" data-group-id="5836966134-49">(</span><span class="p" data-group-id="5836966134-50">[</span><span class="w">
      </span><span class="n">ascii_string</span><span class="p" data-group-id="5836966134-51">(</span><span class="p" data-group-id="5836966134-52">[</span><span class="mi">33</span><span class="o">..</span><span class="mi">255</span><span class="p" data-group-id="5836966134-52">]</span><span class="p">,</span><span class="w"> </span><span class="ss">min</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="p" data-group-id="5836966134-51">)</span><span class="p">,</span><span class="w">
      </span><span class="n">empty</span><span class="p" data-group-id="5836966134-53">(</span><span class="p" data-group-id="5836966134-53">)</span><span class="w"> </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">replace</span><span class="p" data-group-id="5836966134-54">(</span><span class="s">~S|&quot;&quot;|</span><span class="p" data-group-id="5836966134-54">)</span><span class="w">
    </span><span class="p" data-group-id="5836966134-50">]</span><span class="p" data-group-id="5836966134-49">)</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">unwrap_and_tag</span><span class="p" data-group-id="5836966134-55">(</span><span class="ss">:invalid_field_value</span><span class="p" data-group-id="5836966134-55">)</span><span class="w">
  </span><span class="k" data-group-id="5836966134-48">end</span><span class="w">

  </span><span class="kd">def</span><span class="w"> </span><span class="nf">check_for_no_invalid_field_values!</span><span class="p" data-group-id="5836966134-56">(</span><span class="p" data-group-id="5836966134-57">%{</span><span class="ss">value</span><span class="p">:</span><span class="w"> </span><span class="p" data-group-id="5836966134-58">{</span><span class="ss">:invalid_field_value</span><span class="p">,</span><span class="w"> </span><span class="n">value</span><span class="p" data-group-id="5836966134-58">}</span><span class="p" data-group-id="5836966134-57">}</span><span class="p" data-group-id="5836966134-56">)</span><span class="w"> </span><span class="k" data-group-id="5836966134-59">do</span><span class="w">
    </span><span class="k">raise</span><span class="w"> </span><span class="s">&quot;invalid filter value: </span><span class="si" data-group-id="5836966134-60">#{</span><span class="n">value</span><span class="si" data-group-id="5836966134-60">}</span><span class="s">&quot;</span><span class="w">
  </span><span class="k" data-group-id="5836966134-59">end</span><span class="w">

  </span><span class="kd">def</span><span class="w"> </span><span class="nf">check_for_no_invalid_field_values!</span><span class="p" data-group-id="5836966134-61">(</span><span class="n">filter</span><span class="p" data-group-id="5836966134-61">)</span><span class="p">,</span><span class="w"> </span><span class="ss">do</span><span class="p">:</span><span class="w"> </span><span class="n">filter</span><span class="w">

  </span><span class="kd">def</span><span class="w"> </span><span class="nf">to_filter</span><span class="p" data-group-id="5836966134-62">(</span><span class="n">args</span><span class="p" data-group-id="5836966134-62">)</span><span class="w"> </span><span class="k" data-group-id="5836966134-63">do</span><span class="w">
    </span><span class="p" data-group-id="5836966134-64">%{</span><span class="w">
      </span><span class="ss">field</span><span class="p">:</span><span class="w"> </span><span class="nc">Keyword</span><span class="o">.</span><span class="n">fetch!</span><span class="p" data-group-id="5836966134-65">(</span><span class="n">args</span><span class="p">,</span><span class="w"> </span><span class="ss">:field</span><span class="p" data-group-id="5836966134-65">)</span><span class="p">,</span><span class="w">
      </span><span class="ss">op</span><span class="p">:</span><span class="w"> </span><span class="nc">Keyword</span><span class="o">.</span><span class="n">fetch!</span><span class="p" data-group-id="5836966134-66">(</span><span class="n">args</span><span class="p">,</span><span class="w"> </span><span class="ss">:operator</span><span class="p" data-group-id="5836966134-66">)</span><span class="p">,</span><span class="w">
      </span><span class="ss">value</span><span class="p">:</span><span class="w"> </span><span class="nc">Keyword</span><span class="o">.</span><span class="n">fetch!</span><span class="p" data-group-id="5836966134-67">(</span><span class="n">args</span><span class="p">,</span><span class="w"> </span><span class="ss">:value</span><span class="p" data-group-id="5836966134-67">)</span><span class="w">
    </span><span class="p" data-group-id="5836966134-64">}</span><span class="w">
  </span><span class="k" data-group-id="5836966134-63">end</span><span class="w">

  </span><span class="c1"># ...</span><span class="w">
</span><span class="k" data-group-id="5836966134-1">end</span></code></pre>
<p>
Here we match a field clause with the field name, field operator, and field value. The field must only contain letters or <code class="inline">_</code>. The operator can be any of the <code class="inline">&lt;</code>, <code class="inline">&gt;</code>, <code class="inline">&lt;=</code>, <code class="inline">&gt;=</code> comparisons. The value can be alphanumeric and will be parsed as either an integer or a string. We’ll also match invalid values since we want to catch everything up to the next whitespace and raise an error if there are invalid characters. Finally, it all gets mapped into a filter that can be passed on to <code class="inline">Flop</code>.</p>
<p>
The last thing is to match the query operator that goes in between the field clauses:</p>
<pre><code class="makeup elixir"><span class="kd">defmodule</span><span class="w"> </span><span class="nc">MyAppWeb.QueryParser.Helpers</span><span class="w"> </span><span class="k" data-group-id="8858925785-1">do</span><span class="w">
  </span><span class="c1"># ...</span><span class="w">

  </span><span class="kd">def</span><span class="w"> </span><span class="nf">whitespace</span><span class="w"> </span><span class="k" data-group-id="8858925785-2">do</span><span class="w">
    </span><span class="n">ascii_string</span><span class="p" data-group-id="8858925785-3">(</span><span class="p" data-group-id="8858925785-4">[</span><span class="sc">?\s</span><span class="p">,</span><span class="w"> </span><span class="sc">?\n</span><span class="p" data-group-id="8858925785-4">]</span><span class="p">,</span><span class="w"> </span><span class="ss">min</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="p" data-group-id="8858925785-3">)</span><span class="w">
  </span><span class="k" data-group-id="8858925785-2">end</span><span class="w">

  </span><span class="kd">def</span><span class="w"> </span><span class="nf">query_operator</span><span class="w"> </span><span class="k" data-group-id="8858925785-5">do</span><span class="w">
    </span><span class="n">choice</span><span class="p" data-group-id="8858925785-6">(</span><span class="p" data-group-id="8858925785-7">[</span><span class="w">
      </span><span class="n">string</span><span class="p" data-group-id="8858925785-8">(</span><span class="s">&quot;AND&quot;</span><span class="p" data-group-id="8858925785-8">)</span><span class="p">,</span><span class="w">
      </span><span class="n">invalid_match_all_query_operator</span><span class="p" data-group-id="8858925785-9">(</span><span class="p" data-group-id="8858925785-9">)</span><span class="w">
    </span><span class="p" data-group-id="8858925785-7">]</span><span class="p" data-group-id="8858925785-6">)</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">unwrap_and_tag</span><span class="p" data-group-id="8858925785-10">(</span><span class="ss">:operator</span><span class="p" data-group-id="8858925785-10">)</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">map</span><span class="p" data-group-id="8858925785-11">(</span><span class="p" data-group-id="8858925785-12">{</span><span class="ss">:check_for_no_invalid_query_operator_values!</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="8858925785-13">[</span><span class="p" data-group-id="8858925785-13">]</span><span class="p" data-group-id="8858925785-12">}</span><span class="p" data-group-id="8858925785-11">)</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">label</span><span class="p" data-group-id="8858925785-14">(</span><span class="s">&quot;query clause operator&quot;</span><span class="p" data-group-id="8858925785-14">)</span><span class="w">
  </span><span class="k" data-group-id="8858925785-5">end</span><span class="w">

  </span><span class="kd">def</span><span class="w"> </span><span class="nf">invalid_match_all_query_operator</span><span class="w"> </span><span class="k" data-group-id="8858925785-15">do</span><span class="w">
    </span><span class="n">ascii_string</span><span class="p" data-group-id="8858925785-16">(</span><span class="p" data-group-id="8858925785-17">[</span><span class="mi">33</span><span class="o">..</span><span class="mi">255</span><span class="p" data-group-id="8858925785-17">]</span><span class="p">,</span><span class="w"> </span><span class="ss">min</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="p" data-group-id="8858925785-16">)</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">unwrap_and_tag</span><span class="p" data-group-id="8858925785-18">(</span><span class="ss">:invalid_query_operator</span><span class="p" data-group-id="8858925785-18">)</span><span class="w">
  </span><span class="k" data-group-id="8858925785-15">end</span><span class="w">

  </span><span class="kd">def</span><span class="w"> </span><span class="nf">check_for_no_invalid_query_operator_values!</span><span class="p" data-group-id="8858925785-19">(</span><span class="p" data-group-id="8858925785-20">{</span><span class="ss">:operator</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="8858925785-21">{</span><span class="ss">:invalid_query_operator</span><span class="p">,</span><span class="w"> </span><span class="n">operator</span><span class="p" data-group-id="8858925785-21">}</span><span class="p" data-group-id="8858925785-20">}</span><span class="p" data-group-id="8858925785-19">)</span><span class="w"> </span><span class="k" data-group-id="8858925785-22">do</span><span class="w">
    </span><span class="k">raise</span><span class="w"> </span><span class="s">&quot;invalid query operator: </span><span class="si" data-group-id="8858925785-23">#{</span><span class="n">operator</span><span class="si" data-group-id="8858925785-23">}</span><span class="s">&quot;</span><span class="w">
  </span><span class="k" data-group-id="8858925785-22">end</span><span class="w">

  </span><span class="kd">def</span><span class="w"> </span><span class="nf">check_for_no_invalid_query_operator_values!</span><span class="p" data-group-id="8858925785-24">(</span><span class="n">filter</span><span class="p" data-group-id="8858925785-24">)</span><span class="p">,</span><span class="w"> </span><span class="ss">do</span><span class="p">:</span><span class="w"> </span><span class="n">filter</span><span class="w">

  </span><span class="c1"># ...</span><span class="w">
</span><span class="k" data-group-id="8858925785-1">end</span></code></pre>
<p>
We’ll only support <code class="inline">AND</code> since that’s all we can do with <code class="inline">Flop</code>. And again we are matching any characters so we can raise an error for an invalid operator.</p>
<p>
Here we are, our first search DSL, ready to be extended!</p>
<p>
There is a lot that can (and should) be added to this. More field operators, ensuring that a query operator has a field clause on the right side, nested clauses. But this gives us a great starting point for our search DSL!</p>
<details><summary class="cursor-pointer">
Tests for <code class="inline">MyAppWeb.QueryParser</code>.
</summary>
<pre><code class="makeup elixir"><span class="kd">defmodule</span><span class="w"> </span><span class="nc">MyAppWeb.QueryParserTest</span><span class="w"> </span><span class="k" data-group-id="7823016458-1">do</span><span class="w">
  </span><span class="kn">use</span><span class="w"> </span><span class="nc">MyAppWeb.ConnCase</span><span class="w">

  </span><span class="kn">alias</span><span class="w"> </span><span class="nc">MyAppWeb.QueryParser</span><span class="w">

  </span><span class="n">describe</span><span class="w"> </span><span class="s">&quot;parse/2&quot;</span><span class="w"> </span><span class="k" data-group-id="7823016458-2">do</span><span class="w">
    </span><span class="n">test</span><span class="w"> </span><span class="s">&quot;with no field&quot;</span><span class="w"> </span><span class="k" data-group-id="7823016458-3">do</span><span class="w">
      </span><span class="n">assert</span><span class="w"> </span><span class="p" data-group-id="7823016458-4">{</span><span class="ss">:error</span><span class="p">,</span><span class="w"> </span><span class="n">error</span><span class="p" data-group-id="7823016458-4">}</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">QueryParser</span><span class="o">.</span><span class="n">parse</span><span class="p" data-group-id="7823016458-5">(</span><span class="s">&quot;&quot;</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="7823016458-6">[</span><span class="ss">field</span><span class="p">:</span><span class="w"> </span><span class="ss">:string</span><span class="p" data-group-id="7823016458-6">]</span><span class="p" data-group-id="7823016458-5">)</span><span class="w">
      </span><span class="n">assert</span><span class="w"> </span><span class="n">error</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="s">&quot;expected filter field while processing field filter clause&quot;</span><span class="w">
    </span><span class="k" data-group-id="7823016458-3">end</span><span class="w">

    </span><span class="n">test</span><span class="w"> </span><span class="s">&quot;with no operator&quot;</span><span class="w"> </span><span class="k" data-group-id="7823016458-7">do</span><span class="w">
      </span><span class="n">assert</span><span class="w"> </span><span class="p" data-group-id="7823016458-8">{</span><span class="ss">:error</span><span class="p">,</span><span class="w"> </span><span class="n">error</span><span class="p" data-group-id="7823016458-8">}</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">QueryParser</span><span class="o">.</span><span class="n">parse</span><span class="p" data-group-id="7823016458-9">(</span><span class="s">&quot;field&quot;</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="7823016458-10">[</span><span class="ss">field</span><span class="p">:</span><span class="w"> </span><span class="ss">:string</span><span class="p" data-group-id="7823016458-10">]</span><span class="p" data-group-id="7823016458-9">)</span><span class="w">
      </span><span class="n">assert</span><span class="w"> </span><span class="n">error</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="s">&quot;expected filter operator while processing field filter clause&quot;</span><span class="w">
    </span><span class="k" data-group-id="7823016458-7">end</span><span class="w">

    </span><span class="n">test</span><span class="w"> </span><span class="s">&quot;with no value&quot;</span><span class="w"> </span><span class="k" data-group-id="7823016458-11">do</span><span class="w">
      </span><span class="n">assert_raise</span><span class="w"> </span><span class="nc">RuntimeError</span><span class="p">,</span><span class="w"> </span><span class="s">&quot;invalid filter value: </span><span class="se">\&quot;</span><span class="se">\&quot;</span><span class="s">&quot;</span><span class="p">,</span><span class="w"> </span><span class="k" data-group-id="7823016458-12">fn</span><span class="w"> </span><span class="o">-&gt;</span><span class="w">
        </span><span class="nc">QueryParser</span><span class="o">.</span><span class="n">parse</span><span class="p" data-group-id="7823016458-13">(</span><span class="s">&quot;field&lt;&quot;</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="7823016458-14">[</span><span class="ss">field</span><span class="p">:</span><span class="w"> </span><span class="ss">:string</span><span class="p" data-group-id="7823016458-14">]</span><span class="p" data-group-id="7823016458-13">)</span><span class="w">
      </span><span class="k" data-group-id="7823016458-12">end</span><span class="w">
    </span><span class="k" data-group-id="7823016458-11">end</span><span class="w">

    </span><span class="n">test</span><span class="w"> </span><span class="s">&quot;with invalid field&quot;</span><span class="w"> </span><span class="k" data-group-id="7823016458-15">do</span><span class="w">
      </span><span class="n">assert</span><span class="w"> </span><span class="p" data-group-id="7823016458-16">{</span><span class="ss">:error</span><span class="p">,</span><span class="w"> </span><span class="n">error</span><span class="p" data-group-id="7823016458-16">}</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">QueryParser</span><span class="o">.</span><span class="n">parse</span><span class="p" data-group-id="7823016458-17">(</span><span class="s">&quot;$$$$=value&quot;</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="7823016458-18">[</span><span class="ss">field</span><span class="p">:</span><span class="w"> </span><span class="ss">:string</span><span class="p" data-group-id="7823016458-18">]</span><span class="p" data-group-id="7823016458-17">)</span><span class="w">
      </span><span class="n">assert</span><span class="w"> </span><span class="n">error</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="s">&quot;expected filter field while processing field filter clause&quot;</span><span class="w">
    </span><span class="k" data-group-id="7823016458-15">end</span><span class="w">

    </span><span class="n">test</span><span class="w"> </span><span class="s">&quot;with invalid operator&quot;</span><span class="w"> </span><span class="k" data-group-id="7823016458-19">do</span><span class="w">
      </span><span class="n">assert</span><span class="w"> </span><span class="p" data-group-id="7823016458-20">{</span><span class="ss">:error</span><span class="p">,</span><span class="w"> </span><span class="n">error</span><span class="p" data-group-id="7823016458-20">}</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">QueryParser</span><span class="o">.</span><span class="n">parse</span><span class="p" data-group-id="7823016458-21">(</span><span class="s">&quot;field=value&quot;</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="7823016458-22">[</span><span class="ss">field</span><span class="p">:</span><span class="w"> </span><span class="ss">:string</span><span class="p" data-group-id="7823016458-22">]</span><span class="p" data-group-id="7823016458-21">)</span><span class="w">
      </span><span class="n">assert</span><span class="w"> </span><span class="n">error</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="s">&quot;expected filter operator while processing field filter clause&quot;</span><span class="w">
    </span><span class="k" data-group-id="7823016458-19">end</span><span class="w">

    </span><span class="n">test</span><span class="w"> </span><span class="s">&quot;with invalid value&quot;</span><span class="w"> </span><span class="k" data-group-id="7823016458-23">do</span><span class="w">
      </span><span class="n">assert_raise</span><span class="w"> </span><span class="nc">RuntimeError</span><span class="p">,</span><span class="w"> </span><span class="s">&quot;invalid filter value: $$$$&quot;</span><span class="p">,</span><span class="w"> </span><span class="k" data-group-id="7823016458-24">fn</span><span class="w"> </span><span class="o">-&gt;</span><span class="w">
        </span><span class="nc">QueryParser</span><span class="o">.</span><span class="n">parse</span><span class="p" data-group-id="7823016458-25">(</span><span class="s">&quot;field&lt;$$$$&quot;</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="7823016458-26">[</span><span class="ss">field</span><span class="p">:</span><span class="w"> </span><span class="ss">:string</span><span class="p" data-group-id="7823016458-26">]</span><span class="p" data-group-id="7823016458-25">)</span><span class="w">
      </span><span class="k" data-group-id="7823016458-24">end</span><span class="w">
    </span><span class="k" data-group-id="7823016458-23">end</span><span class="w">

    </span><span class="n">test</span><span class="w"> </span><span class="s">&quot;with missing field in opts&quot;</span><span class="w"> </span><span class="k" data-group-id="7823016458-27">do</span><span class="w">
      </span><span class="n">assert</span><span class="w"> </span><span class="p" data-group-id="7823016458-28">{</span><span class="ss">:error</span><span class="p">,</span><span class="w"> </span><span class="n">error</span><span class="p" data-group-id="7823016458-28">}</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">QueryParser</span><span class="o">.</span><span class="n">parse</span><span class="p" data-group-id="7823016458-29">(</span><span class="s">&quot;field&lt;value&quot;</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="7823016458-30">[</span><span class="ss">other_field</span><span class="p">:</span><span class="w"> </span><span class="ss">:string</span><span class="p" data-group-id="7823016458-30">]</span><span class="p" data-group-id="7823016458-29">)</span><span class="w">
      </span><span class="n">assert</span><span class="w"> </span><span class="n">error</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="s">&quot;unknown filter fields, got field&quot;</span><span class="w">
    </span><span class="k" data-group-id="7823016458-27">end</span><span class="w">

    </span><span class="n">test</span><span class="w"> </span><span class="s">&quot;with valid expression&quot;</span><span class="w"> </span><span class="k" data-group-id="7823016458-31">do</span><span class="w">
      </span><span class="n">assert</span><span class="w"> </span><span class="p" data-group-id="7823016458-32">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="7823016458-33">[</span><span class="n">filter</span><span class="p" data-group-id="7823016458-33">]</span><span class="p" data-group-id="7823016458-32">}</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">QueryParser</span><span class="o">.</span><span class="n">parse</span><span class="p" data-group-id="7823016458-34">(</span><span class="s">&quot;field&lt;value&quot;</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="7823016458-35">[</span><span class="ss">field</span><span class="p">:</span><span class="w"> </span><span class="ss">:string</span><span class="p" data-group-id="7823016458-35">]</span><span class="p" data-group-id="7823016458-34">)</span><span class="w">
      </span><span class="n">assert</span><span class="w"> </span><span class="n">filter</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="p" data-group-id="7823016458-36">%{</span><span class="ss">field</span><span class="p">:</span><span class="w"> </span><span class="s">&quot;field&quot;</span><span class="p">,</span><span class="w"> </span><span class="ss">op</span><span class="p">:</span><span class="w"> </span><span class="ss">:&lt;</span><span class="p">,</span><span class="w"> </span><span class="ss">value</span><span class="p">:</span><span class="w"> </span><span class="s">&quot;value&quot;</span><span class="p" data-group-id="7823016458-36">}</span><span class="w">
    </span><span class="k" data-group-id="7823016458-31">end</span><span class="w">

    </span><span class="n">test</span><span class="w"> </span><span class="s">&quot;with invalid datetime type&quot;</span><span class="w"> </span><span class="k" data-group-id="7823016458-37">do</span><span class="w">
      </span><span class="n">assert</span><span class="w"> </span><span class="p" data-group-id="7823016458-38">{</span><span class="ss">:error</span><span class="p">,</span><span class="w"> </span><span class="n">error</span><span class="p" data-group-id="7823016458-38">}</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">QueryParser</span><span class="o">.</span><span class="n">parse</span><span class="p" data-group-id="7823016458-39">(</span><span class="s">&quot;field&lt;invalid&quot;</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="7823016458-40">[</span><span class="ss">field</span><span class="p">:</span><span class="w"> </span><span class="ss">:utc_datetime_usec</span><span class="p" data-group-id="7823016458-40">]</span><span class="p" data-group-id="7823016458-39">)</span><span class="w">
      </span><span class="n">assert</span><span class="w"> </span><span class="n">error</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="s">&quot;unexpected timestamp value for field, got invalid&quot;</span><span class="w">
    </span><span class="k" data-group-id="7823016458-37">end</span><span class="w">

    </span><span class="n">test</span><span class="w"> </span><span class="s">&quot;with invalid datetime value&quot;</span><span class="w"> </span><span class="k" data-group-id="7823016458-41">do</span><span class="w">
      </span><span class="n">assert</span><span class="w"> </span><span class="p" data-group-id="7823016458-42">{</span><span class="ss">:error</span><span class="p">,</span><span class="w"> </span><span class="n">error</span><span class="p" data-group-id="7823016458-42">}</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">QueryParser</span><span class="o">.</span><span class="n">parse</span><span class="p" data-group-id="7823016458-43">(</span><span class="s">&quot;field&lt;</span><span class="si" data-group-id="7823016458-44">#{</span><span class="mi">253_402_300_800</span><span class="si" data-group-id="7823016458-44">}</span><span class="s">&quot;</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="7823016458-45">[</span><span class="ss">field</span><span class="p">:</span><span class="w"> </span><span class="ss">:utc_datetime_usec</span><span class="p" data-group-id="7823016458-45">]</span><span class="p" data-group-id="7823016458-43">)</span><span class="w">
      </span><span class="n">assert</span><span class="w"> </span><span class="n">error</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="s">&quot;unexpected timestamp value for field, got </span><span class="si" data-group-id="7823016458-46">#{</span><span class="mi">253_402_300_800</span><span class="si" data-group-id="7823016458-46">}</span><span class="s">&quot;</span><span class="w">
    </span><span class="k" data-group-id="7823016458-41">end</span><span class="w">

    </span><span class="n">test</span><span class="w"> </span><span class="s">&quot;with datetime value&quot;</span><span class="w"> </span><span class="k" data-group-id="7823016458-47">do</span><span class="w">
      </span><span class="n">assert</span><span class="w"> </span><span class="p" data-group-id="7823016458-48">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="7823016458-49">[</span><span class="n">filter</span><span class="p" data-group-id="7823016458-49">]</span><span class="p" data-group-id="7823016458-48">}</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">QueryParser</span><span class="o">.</span><span class="n">parse</span><span class="p" data-group-id="7823016458-50">(</span><span class="s">&quot;field&lt;</span><span class="si" data-group-id="7823016458-51">#{</span><span class="mi">1_464_096_368</span><span class="si" data-group-id="7823016458-51">}</span><span class="s">&quot;</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="7823016458-52">[</span><span class="ss">field</span><span class="p">:</span><span class="w"> </span><span class="ss">:utc_datetime_usec</span><span class="p" data-group-id="7823016458-52">]</span><span class="p" data-group-id="7823016458-50">)</span><span class="w">
      </span><span class="n">assert</span><span class="w"> </span><span class="n">filter</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="p" data-group-id="7823016458-53">%{</span><span class="ss">field</span><span class="p">:</span><span class="w"> </span><span class="s">&quot;field&quot;</span><span class="p">,</span><span class="w"> </span><span class="ss">op</span><span class="p">:</span><span class="w"> </span><span class="ss">:&lt;</span><span class="p">,</span><span class="w"> </span><span class="ss">value</span><span class="p">:</span><span class="w"> </span><span class="ld">~U[2016-05-24 13:26:08.000Z]</span><span class="p" data-group-id="7823016458-53">}</span><span class="w">
    </span><span class="k" data-group-id="7823016458-47">end</span><span class="w">

    </span><span class="n">test</span><span class="w"> </span><span class="s">&quot;with invalid part&quot;</span><span class="w"> </span><span class="k" data-group-id="7823016458-54">do</span><span class="w">
      </span><span class="n">assert</span><span class="w"> </span><span class="p" data-group-id="7823016458-55">{</span><span class="ss">:error</span><span class="p">,</span><span class="w"> </span><span class="n">error</span><span class="p" data-group-id="7823016458-55">}</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">QueryParser</span><span class="o">.</span><span class="n">parse</span><span class="p" data-group-id="7823016458-56">(</span><span class="s">&quot;field&lt;value &quot;</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="7823016458-57">[</span><span class="ss">field</span><span class="p">:</span><span class="w"> </span><span class="ss">:string</span><span class="p" data-group-id="7823016458-57">]</span><span class="p" data-group-id="7823016458-56">)</span><span class="w">
      </span><span class="n">assert</span><span class="w"> </span><span class="n">error</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="s">&quot;couldn&#39;t process all of the query, got </span><span class="se">\&quot;</span><span class="s"> </span><span class="se">\&quot;</span><span class="s">&quot;</span><span class="w">
    </span><span class="k" data-group-id="7823016458-54">end</span><span class="w">

    </span><span class="n">test</span><span class="w"> </span><span class="s">&quot;with multiple missing fields in opts&quot;</span><span class="w"> </span><span class="k" data-group-id="7823016458-58">do</span><span class="w">
      </span><span class="n">assert</span><span class="w"> </span><span class="p" data-group-id="7823016458-59">{</span><span class="ss">:error</span><span class="p">,</span><span class="w"> </span><span class="n">error</span><span class="p" data-group-id="7823016458-59">}</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">QueryParser</span><span class="o">.</span><span class="n">parse</span><span class="p" data-group-id="7823016458-60">(</span><span class="s">&quot;first_field&lt;test AND second_field&lt;test&quot;</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="7823016458-61">[</span><span class="ss">field</span><span class="p">:</span><span class="w"> </span><span class="ss">:string</span><span class="p" data-group-id="7823016458-61">]</span><span class="p" data-group-id="7823016458-60">)</span><span class="w">
      </span><span class="n">assert</span><span class="w"> </span><span class="n">error</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="s">&quot;unknown filter fields, got first_field, second_field&quot;</span><span class="w">
    </span><span class="k" data-group-id="7823016458-58">end</span><span class="w">

    </span><span class="n">test</span><span class="w"> </span><span class="s">&quot;with invalid clause operator&quot;</span><span class="w"> </span><span class="k" data-group-id="7823016458-62">do</span><span class="w">
      </span><span class="n">expr</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s">&quot;first_field&lt;value1 OR second_field&gt;value2&quot;</span><span class="w">
      </span><span class="n">opts</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p" data-group-id="7823016458-63">[</span><span class="ss">first_field</span><span class="p">:</span><span class="w"> </span><span class="ss">:string</span><span class="p">,</span><span class="w"> </span><span class="ss">second_field</span><span class="p">:</span><span class="w"> </span><span class="ss">:string</span><span class="p" data-group-id="7823016458-63">]</span><span class="w">

      </span><span class="n">assert_raise</span><span class="w"> </span><span class="nc">RuntimeError</span><span class="p">,</span><span class="w"> </span><span class="s">&quot;invalid query operator: OR&quot;</span><span class="p">,</span><span class="w"> </span><span class="k" data-group-id="7823016458-64">fn</span><span class="w"> </span><span class="o">-&gt;</span><span class="w">
        </span><span class="nc">QueryParser</span><span class="o">.</span><span class="n">parse</span><span class="p" data-group-id="7823016458-65">(</span><span class="n">expr</span><span class="p">,</span><span class="w"> </span><span class="n">opts</span><span class="p" data-group-id="7823016458-65">)</span><span class="w">
      </span><span class="k" data-group-id="7823016458-64">end</span><span class="w">
    </span><span class="k" data-group-id="7823016458-62">end</span><span class="w">

    </span><span class="n">test</span><span class="w"> </span><span class="s">&quot;with multiple valid expressions&quot;</span><span class="w"> </span><span class="k" data-group-id="7823016458-66">do</span><span class="w">
      </span><span class="n">expr</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s">&quot;first_field&lt;value1 AND second_field&gt;value2&quot;</span><span class="w">
      </span><span class="n">opts</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p" data-group-id="7823016458-67">[</span><span class="ss">first_field</span><span class="p">:</span><span class="w"> </span><span class="ss">:string</span><span class="p">,</span><span class="w"> </span><span class="ss">second_field</span><span class="p">:</span><span class="w"> </span><span class="ss">:string</span><span class="p" data-group-id="7823016458-67">]</span><span class="w">

      </span><span class="n">assert</span><span class="w"> </span><span class="p" data-group-id="7823016458-68">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="7823016458-69">[</span><span class="n">filter_1</span><span class="p">,</span><span class="w"> </span><span class="n">filter_2</span><span class="p" data-group-id="7823016458-69">]</span><span class="p" data-group-id="7823016458-68">}</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">QueryParser</span><span class="o">.</span><span class="n">parse</span><span class="p" data-group-id="7823016458-70">(</span><span class="n">expr</span><span class="p">,</span><span class="w"> </span><span class="n">opts</span><span class="p" data-group-id="7823016458-70">)</span><span class="w">
      </span><span class="n">assert</span><span class="w"> </span><span class="n">filter_1</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="p" data-group-id="7823016458-71">%{</span><span class="ss">field</span><span class="p">:</span><span class="w"> </span><span class="s">&quot;first_field&quot;</span><span class="p">,</span><span class="w"> </span><span class="ss">op</span><span class="p">:</span><span class="w"> </span><span class="ss">:&lt;</span><span class="p">,</span><span class="w"> </span><span class="ss">value</span><span class="p">:</span><span class="w"> </span><span class="s">&quot;value1&quot;</span><span class="p" data-group-id="7823016458-71">}</span><span class="w">
      </span><span class="n">assert</span><span class="w"> </span><span class="n">filter_2</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="p" data-group-id="7823016458-72">%{</span><span class="ss">field</span><span class="p">:</span><span class="w"> </span><span class="s">&quot;second_field&quot;</span><span class="p">,</span><span class="w"> </span><span class="ss">op</span><span class="p">:</span><span class="w"> </span><span class="ss">:&gt;</span><span class="p">,</span><span class="w"> </span><span class="ss">value</span><span class="p">:</span><span class="w"> </span><span class="s">&quot;value2&quot;</span><span class="p" data-group-id="7823016458-72">}</span><span class="w">
    </span><span class="k" data-group-id="7823016458-66">end</span><span class="w">
  </span><span class="k" data-group-id="7823016458-2">end</span><span class="w">
</span><span class="k" data-group-id="7823016458-1">end</span></code></pre>
</details>

      ]]>
    </content>
  </entry>
  
  <entry>
    <title>Hierarchical tree depth filter in Ecto</title>
    <link href="https://danschultzer.com/posts/hierarchical-tree-depth-filter-in-ecto" />
    <id>https://danschultzer.com/posts/hierarchical-tree-depth-filter-in-ecto</id>
    <updated>2023-12-16T00:00:00Z</updated>
    <summary>Using CTE in Ecto to filter out a certain depth of nodes starting from the leaf nodes</summary>
    <content type="html">
      <![CDATA[
        <p>
I was dealing with an interesting problem working on a hierarchical model. What if we want to exclude all nodes to a certain depth? We have a tree looking as seen below and want to exclude all nodes at depth <code class="inline">1</code> (which would be nodes <code class="inline">D</code>, <code class="inline">F</code>, and <code class="inline">C</code>):</p>
<pre><code>       A
      / \
     B   C
    / \
   D   E
        \
         F</code></pre>
<p>
Our schema references the parent node:</p>
<pre><code class="makeup elixir"><span class="kd">defmodule</span><span class="w"> </span><span class="nc">MyApp.Tree.Node</span><span class="w"> </span><span class="k" data-group-id="1795251166-1">do</span><span class="w">
  </span><span class="kn">use</span><span class="w"> </span><span class="nc">Ecto.Schema</span><span class="w">

  </span><span class="n">schema</span><span class="w"> </span><span class="s">&quot;nodes&quot;</span><span class="w"> </span><span class="k" data-group-id="1795251166-2">do</span><span class="w">
    </span><span class="n">belongs_to</span><span class="w"> </span><span class="ss">:parent_node</span><span class="p">,</span><span class="w"> </span><span class="nc">Node</span><span class="w">

    </span><span class="n">timestamps</span><span class="p" data-group-id="1795251166-3">(</span><span class="p" data-group-id="1795251166-3">)</span><span class="w">
  </span><span class="k" data-group-id="1795251166-2">end</span><span class="w">
</span><span class="k" data-group-id="1795251166-1">end</span></code></pre>
<p>
We’ll use CTE to recursively go from the leaf nodes up the branch until we’ve reached the depth we want to exclude. This will result in a list of all the nodes we must exclude in our query.</p>
<pre><code class="makeup elixir"><span class="na">@doc</span><span class="w"> </span><span class="s">&quot;&quot;&quot;
This is a CTE that will fetch the node ids for n depth from leaf nodes.
&quot;&quot;&quot;</span><span class="w">
</span><span class="kd">def</span><span class="w"> </span><span class="nf">filter_node_depth_query</span><span class="p" data-group-id="4813322800-1">(</span><span class="n">depth</span><span class="p" data-group-id="4813322800-1">)</span><span class="w"> </span><span class="ow">when</span><span class="w"> </span><span class="n">depth</span><span class="w"> </span><span class="o">&gt;</span><span class="w"> </span><span class="mi">0</span><span class="w"> </span><span class="k" data-group-id="4813322800-2">do</span><span class="w">
  </span><span class="c1"># Select all parent node ids</span><span class="w">
  </span><span class="n">parent_node_ids_query</span><span class="w"> </span><span class="o">=</span><span class="w">
    </span><span class="nc">Node</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">where</span><span class="p" data-group-id="4813322800-3">(</span><span class="p" data-group-id="4813322800-4">[</span><span class="n">n</span><span class="p" data-group-id="4813322800-4">]</span><span class="p">,</span><span class="w"> </span><span class="ow">not</span><span class="w"> </span><span class="n">is_nil</span><span class="p" data-group-id="4813322800-5">(</span><span class="n">n</span><span class="o">.</span><span class="n">parent_node_id</span><span class="p" data-group-id="4813322800-5">)</span><span class="p" data-group-id="4813322800-3">)</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">select</span><span class="p" data-group-id="4813322800-6">(</span><span class="p" data-group-id="4813322800-7">[</span><span class="n">n</span><span class="p" data-group-id="4813322800-7">]</span><span class="p">,</span><span class="w"> </span><span class="n">n</span><span class="o">.</span><span class="n">parent_node_id</span><span class="p" data-group-id="4813322800-6">)</span><span class="w">

  </span><span class="c1"># Select all leaf nodes as starting depth</span><span class="w">
  </span><span class="n">leaf_nodes_query</span><span class="w"> </span><span class="o">=</span><span class="w">
    </span><span class="nc">Node</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">where</span><span class="p" data-group-id="4813322800-8">(</span><span class="p" data-group-id="4813322800-9">[</span><span class="n">n</span><span class="p" data-group-id="4813322800-9">]</span><span class="p">,</span><span class="w"> </span><span class="n">n</span><span class="o">.</span><span class="n">id</span><span class="w"> </span><span class="ow">not</span><span class="w"> </span><span class="ow">in</span><span class="w"> </span><span class="n">subquery</span><span class="p" data-group-id="4813322800-10">(</span><span class="n">parent_node_ids_query</span><span class="p" data-group-id="4813322800-10">)</span><span class="p" data-group-id="4813322800-8">)</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">select</span><span class="p" data-group-id="4813322800-11">(</span><span class="p" data-group-id="4813322800-12">[</span><span class="n">n</span><span class="p" data-group-id="4813322800-12">]</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="4813322800-13">%{</span><span class="ss">depth</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span><span class="w"> </span><span class="ss">id</span><span class="p">:</span><span class="w"> </span><span class="n">n</span><span class="o">.</span><span class="n">id</span><span class="p">,</span><span class="w"> </span><span class="ss">parent_node_id</span><span class="p">:</span><span class="w"> </span><span class="n">n</span><span class="o">.</span><span class="n">parent_node_id</span><span class="p" data-group-id="4813322800-13">}</span><span class="p" data-group-id="4813322800-11">)</span><span class="w">

  </span><span class="n">nodes_recursion_query</span><span class="w"> </span><span class="o">=</span><span class="w">
    </span><span class="nc">Node</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">join</span><span class="p" data-group-id="4813322800-14">(</span><span class="ss">:inner</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="4813322800-15">[</span><span class="n">pn</span><span class="p" data-group-id="4813322800-15">]</span><span class="p">,</span><span class="w"> </span><span class="n">n</span><span class="w"> </span><span class="ow">in</span><span class="w"> </span><span class="s">&quot;node_tree&quot;</span><span class="p">,</span><span class="w"> </span><span class="ss">on</span><span class="p">:</span><span class="w"> </span><span class="n">n</span><span class="o">.</span><span class="n">parent_node_id</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="n">pn</span><span class="o">.</span><span class="n">id</span><span class="p" data-group-id="4813322800-14">)</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">select</span><span class="p" data-group-id="4813322800-16">(</span><span class="p" data-group-id="4813322800-17">[</span><span class="n">pn</span><span class="p">,</span><span class="w"> </span><span class="n">n</span><span class="p" data-group-id="4813322800-17">]</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="4813322800-18">%{</span><span class="ss">depth</span><span class="p">:</span><span class="w"> </span><span class="n">n</span><span class="o">.</span><span class="n">depth</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span><span class="w"> </span><span class="ss">id</span><span class="p">:</span><span class="w"> </span><span class="n">pn</span><span class="o">.</span><span class="n">id</span><span class="p">,</span><span class="w"> </span><span class="ss">parent_node_id</span><span class="p">:</span><span class="w"> </span><span class="n">pn</span><span class="o">.</span><span class="n">parent_node_id</span><span class="p" data-group-id="4813322800-18">}</span><span class="p" data-group-id="4813322800-16">)</span><span class="w">

    </span><span class="c1"># Cut off at n depth</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">where</span><span class="p" data-group-id="4813322800-19">(</span><span class="p" data-group-id="4813322800-20">[</span><span class="n">pn</span><span class="p">,</span><span class="w"> </span><span class="n">n</span><span class="p" data-group-id="4813322800-20">]</span><span class="p">,</span><span class="w"> </span><span class="n">n</span><span class="o">.</span><span class="n">depth</span><span class="w"> </span><span class="o">&lt;</span><span class="w"> </span><span class="o">^</span><span class="n">n</span><span class="p" data-group-id="4813322800-19">)</span><span class="w">

  </span><span class="n">node_tree_query</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">union_all</span><span class="p" data-group-id="4813322800-21">(</span><span class="n">leaf_nodes_query</span><span class="p">,</span><span class="w"> </span><span class="o">^</span><span class="n">nodes_recursion_query</span><span class="p" data-group-id="4813322800-21">)</span><span class="w">

  </span><span class="p" data-group-id="4813322800-22">{</span><span class="s">&quot;node_tree&quot;</span><span class="p">,</span><span class="w"> </span><span class="nc">Node</span><span class="p" data-group-id="4813322800-22">}</span><span class="w">
  </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">recursive_ctes</span><span class="p" data-group-id="4813322800-23">(</span><span class="no">true</span><span class="p" data-group-id="4813322800-23">)</span><span class="w">
  </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">with_cte</span><span class="p" data-group-id="4813322800-24">(</span><span class="s">&quot;node_tree&quot;</span><span class="p">,</span><span class="w"> </span><span class="ss">as</span><span class="p">:</span><span class="w"> </span><span class="o">^</span><span class="n">node_tree_query</span><span class="p" data-group-id="4813322800-24">)</span><span class="w">
  </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">select</span><span class="p" data-group-id="4813322800-25">(</span><span class="p" data-group-id="4813322800-26">[</span><span class="n">n</span><span class="p" data-group-id="4813322800-26">]</span><span class="p">,</span><span class="w"> </span><span class="n">n</span><span class="o">.</span><span class="n">id</span><span class="p" data-group-id="4813322800-25">)</span><span class="w">
</span><span class="k" data-group-id="4813322800-2">end</span></code></pre>
<p>
We first select all the parent node IDs from our nodes table and then use that subquery to select all the nodes that don’t exist in the list. This is how we get all leaf nodes at starting depth <code class="inline">1</code> because these nodes don’t exist in any node’s <code class="inline">parent_node_id</code>.</p>
<p>
Then we recursively go from the list of leaf nodes up the branch by joining the <code class="inline">id</code> with <code class="inline">parent_node_id</code>, increasing the depth by <code class="inline">1</code> each time.</p>
<p>
Finally, we cut off when we have reached the desired depth.</p>
<p>
This can be used in our query to filter out the resulting list of nodes:</p>
<pre><code class="makeup elixir"><span class="n">node_ids_query</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">filter_node_depth_query</span><span class="p" data-group-id="1273875554-1">(</span><span class="n">depth</span><span class="p" data-group-id="1273875554-1">)</span><span class="w">

</span><span class="nc">Node</span><span class="w">
</span><span class="o">|&gt;</span><span class="w"> </span><span class="n">where</span><span class="p" data-group-id="1273875554-2">(</span><span class="p" data-group-id="1273875554-3">[</span><span class="n">n</span><span class="p" data-group-id="1273875554-3">]</span><span class="p">,</span><span class="w"> </span><span class="n">n</span><span class="o">.</span><span class="n">id</span><span class="w"> </span><span class="ow">not</span><span class="w"> </span><span class="ow">in</span><span class="w"> </span><span class="n">subquery</span><span class="p" data-group-id="1273875554-4">(</span><span class="n">node_ids_query</span><span class="p" data-group-id="1273875554-4">)</span><span class="p" data-group-id="1273875554-2">)</span><span class="w">
</span><span class="o">|&gt;</span><span class="w"> </span><span class="nc">Repo</span><span class="o">.</span><span class="n">all</span><span class="p" data-group-id="1273875554-5">(</span><span class="p" data-group-id="1273875554-5">)</span></code></pre>
<p>
One problem with this CTE is that the recursion will be greedy. To demonstrate let’s exclude all nodes with depth <code class="inline">2</code>:</p>
<pre><code>       A
      / \
     B   C
    / \
   D   E
        \
         F</code></pre>
<p>
This will exclude the nodes <code class="inline">D</code>, <code class="inline">E</code>, <code class="inline">F</code>, and <code class="inline">C</code> which is what we want, but also nodes <code class="inline">B</code> and <code class="inline">A</code>! In my case, it was easily solved because in my tree structure there there’s only one canonical branch. I just had to filter by canonical and non-canonical branches:</p>
<pre><code class="makeup elixir"><span class="n">nodes_recursion_query</span><span class="w"> </span><span class="o">=</span><span class="w">
  </span><span class="nc">Node</span><span class="w">
  </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">join</span><span class="p" data-group-id="2682449260-1">(</span><span class="ss">:inner</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="2682449260-2">[</span><span class="n">pn</span><span class="p" data-group-id="2682449260-2">]</span><span class="p">,</span><span class="w"> </span><span class="n">n</span><span class="w"> </span><span class="ow">in</span><span class="w"> </span><span class="s">&quot;node_tree&quot;</span><span class="p">,</span><span class="w"> </span><span class="ss">on</span><span class="p">:</span><span class="w"> </span><span class="n">n</span><span class="o">.</span><span class="n">parent_node_id</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="n">pn</span><span class="o">.</span><span class="n">id</span><span class="p" data-group-id="2682449260-1">)</span><span class="w">
  </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">select</span><span class="p" data-group-id="2682449260-3">(</span><span class="p" data-group-id="2682449260-4">[</span><span class="n">pn</span><span class="p">,</span><span class="w"> </span><span class="n">n</span><span class="p" data-group-id="2682449260-4">]</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="2682449260-5">%{</span><span class="ss">depth</span><span class="p">:</span><span class="w"> </span><span class="n">n</span><span class="o">.</span><span class="n">depth</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span><span class="w"> </span><span class="ss">id</span><span class="p">:</span><span class="w"> </span><span class="n">pn</span><span class="o">.</span><span class="n">id</span><span class="p">,</span><span class="w"> </span><span class="ss">parent_node_id</span><span class="p">:</span><span class="w"> </span><span class="n">pn</span><span class="o">.</span><span class="n">parent_node_id</span><span class="p">,</span><span class="w"> </span><span class="ss">canonical</span><span class="p">:</span><span class="w"> </span><span class="n">pn</span><span class="o">.</span><span class="n">canonical</span><span class="p" data-group-id="2682449260-5">}</span><span class="p" data-group-id="2682449260-3">)</span><span class="w">

  </span><span class="c1"># Parent node should match the node by canonical branch or non canonical branches</span><span class="w">
  </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">where</span><span class="p" data-group-id="2682449260-6">(</span><span class="p" data-group-id="2682449260-7">[</span><span class="n">pn</span><span class="p">,</span><span class="w"> </span><span class="n">n</span><span class="p" data-group-id="2682449260-7">]</span><span class="p">,</span><span class="w"> </span><span class="n">pn</span><span class="o">.</span><span class="n">canonical</span><span class="w"> </span><span class="ow">and</span><span class="w"> </span><span class="n">n</span><span class="o">.</span><span class="n">canonical</span><span class="p" data-group-id="2682449260-6">)</span><span class="w">
  </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">or_where</span><span class="p" data-group-id="2682449260-8">(</span><span class="p" data-group-id="2682449260-9">[</span><span class="n">pn</span><span class="p">,</span><span class="w"> </span><span class="n">n</span><span class="p" data-group-id="2682449260-9">]</span><span class="p">,</span><span class="w"> </span><span class="ow">not</span><span class="w"> </span><span class="n">pn</span><span class="o">.</span><span class="n">canonical</span><span class="w"> </span><span class="ow">and</span><span class="w"> </span><span class="ow">not</span><span class="w"> </span><span class="n">n</span><span class="o">.</span><span class="n">canonical</span><span class="p" data-group-id="2682449260-8">)</span><span class="w">

  </span><span class="c1"># Cut off at n depth</span><span class="w">
  </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">where</span><span class="p" data-group-id="2682449260-10">(</span><span class="p" data-group-id="2682449260-11">[</span><span class="n">pn</span><span class="p">,</span><span class="w"> </span><span class="n">n</span><span class="p" data-group-id="2682449260-11">]</span><span class="p">,</span><span class="w"> </span><span class="n">n</span><span class="o">.</span><span class="n">depth</span><span class="w"> </span><span class="o">&lt;</span><span class="w"> </span><span class="o">^</span><span class="n">n</span><span class="p" data-group-id="2682449260-10">)</span></code></pre>
<p>
If you don’t have this distinction you’ll have to go through the result set removing the nodes that have child nodes that don’t exist in the result set.</p>

      ]]>
    </content>
  </entry>
  
  <entry>
    <title>Polymorphic embeds in Ecto</title>
    <link href="https://danschultzer.com/posts/polymorphic-embeds-in-ecto" />
    <id>https://danschultzer.com/posts/polymorphic-embeds-in-ecto</id>
    <updated>2023-12-15T00:00:00Z</updated>
    <summary>How to abuse Ecto embeds to do our dirty polymorphic bidding.</summary>
    <content type="html">
      <![CDATA[
        <p>
Recently I had to ensure semi-arbitrary data for an embedded schema could be validated and easily mapped in Phoenix forms. I didn’t need to store this data in the database. After tinkering with it for a bit polymorphic embeds was the solution.</p>
<p>
Digging into Ecto I figured out how I could mostly extend the way the native <a href="https://hexdocs.pm/ecto/3.11.1/embedded-schemas.html">Ecto embed</a> works to get polymorphic embeds working.</p>
<p>
Let’s start first by setting up the embedded schema that requires polymorphic embed:</p>
<pre><code class="makeup elixir"><span class="kd">defmodule</span><span class="w"> </span><span class="nc">MyApp.Accounts.Payment</span><span class="w"> </span><span class="k" data-group-id="0793317490-1">do</span><span class="w">
  </span><span class="kn">use</span><span class="w"> </span><span class="nc">Ecto.Schema</span><span class="w">

  </span><span class="kn">import</span><span class="w"> </span><span class="nc">Ecto.Changeset</span><span class="w">

  </span><span class="kn">alias</span><span class="w"> </span><span class="nc">MyApp.Accounts</span><span class="o">.</span><span class="p" data-group-id="0793317490-2">{</span><span class="nc">Account</span><span class="p">,</span><span class="w"> </span><span class="nc">Type1</span><span class="p">,</span><span class="w"> </span><span class="nc">Type2</span><span class="p" data-group-id="0793317490-2">}</span><span class="w">
  </span><span class="kn">alias</span><span class="w"> </span><span class="nc">MyApp.PolymorphicEmbed</span><span class="w">

  </span><span class="n">embedded_schema</span><span class="w"> </span><span class="k" data-group-id="0793317490-3">do</span><span class="w">
    </span><span class="n">belongs_to</span><span class="w"> </span><span class="ss">:account</span><span class="p">,</span><span class="w"> </span><span class="nc">Account</span><span class="w">

    </span><span class="n">field</span><span class="w"> </span><span class="ss">:amount</span><span class="p">,</span><span class="w"> </span><span class="ss">:integer</span><span class="w">
    </span><span class="n">field</span><span class="w"> </span><span class="ss">:metadata</span><span class="p">,</span><span class="w"> </span><span class="nc">PolymorphicEmbed</span><span class="w">
  </span><span class="k" data-group-id="0793317490-3">end</span><span class="w">

  </span><span class="kd">def</span><span class="w"> </span><span class="nf">changeset</span><span class="p" data-group-id="0793317490-4">(</span><span class="n">payment_or_changeset</span><span class="p">,</span><span class="w"> </span><span class="n">attrs</span><span class="p" data-group-id="0793317490-4">)</span><span class="w"> </span><span class="k" data-group-id="0793317490-5">do</span><span class="w">
    </span><span class="n">payment_or_changeset</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">cast</span><span class="p" data-group-id="0793317490-6">(</span><span class="n">attrs</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="0793317490-7">[</span><span class="ss">:amount</span><span class="p" data-group-id="0793317490-7">]</span><span class="p" data-group-id="0793317490-6">)</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">validate_required</span><span class="p" data-group-id="0793317490-8">(</span><span class="p" data-group-id="0793317490-9">[</span><span class="ss">:amount</span><span class="p">,</span><span class="w"> </span><span class="ss">:account_id</span><span class="p" data-group-id="0793317490-9">]</span><span class="p" data-group-id="0793317490-8">)</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">cast_metadata_embed</span><span class="p" data-group-id="0793317490-10">(</span><span class="p" data-group-id="0793317490-10">)</span><span class="w">
  </span><span class="k" data-group-id="0793317490-5">end</span><span class="w">

  </span><span class="kd">defp</span><span class="w"> </span><span class="nf">cast_metadata_embed</span><span class="p" data-group-id="0793317490-11">(</span><span class="n">changeset</span><span class="p" data-group-id="0793317490-11">)</span><span class="w"> </span><span class="k" data-group-id="0793317490-12">do</span><span class="w">
    </span><span class="n">schema</span><span class="w"> </span><span class="o">=</span><span class="w">
      </span><span class="k">case</span><span class="w"> </span><span class="n">get_field</span><span class="p" data-group-id="0793317490-13">(</span><span class="n">changeset</span><span class="p">,</span><span class="w"> </span><span class="ss">:account</span><span class="p" data-group-id="0793317490-13">)</span><span class="w"> </span><span class="k" data-group-id="0793317490-14">do</span><span class="w">
        </span><span class="p" data-group-id="0793317490-15">%{</span><span class="ss">type</span><span class="p">:</span><span class="w"> </span><span class="s">&quot;type1&quot;</span><span class="p" data-group-id="0793317490-15">}</span><span class="w"> </span><span class="o">-&gt;</span><span class="w"> </span><span class="nc">Type1</span><span class="w">
        </span><span class="p" data-group-id="0793317490-16">%{</span><span class="ss">type</span><span class="p">:</span><span class="w"> </span><span class="s">&quot;type2&quot;</span><span class="p" data-group-id="0793317490-16">}</span><span class="w"> </span><span class="o">-&gt;</span><span class="w"> </span><span class="nc">Type2</span><span class="w">
      </span><span class="k" data-group-id="0793317490-14">end</span><span class="w">

    </span><span class="nc">PolymorphicEmbed</span><span class="o">.</span><span class="n">cast_polymorphic_embed</span><span class="p" data-group-id="0793317490-17">(</span><span class="n">changeset</span><span class="p">,</span><span class="w"> </span><span class="n">schema</span><span class="p">,</span><span class="w"> </span><span class="ss">:metadata</span><span class="p" data-group-id="0793317490-17">)</span><span class="w">
  </span><span class="k" data-group-id="0793317490-12">end</span><span class="w">
</span><span class="k" data-group-id="0793317490-1">end</span></code></pre>
<p>
The metadata schema would look something like this:</p>
<pre><code class="makeup elixir"><span class="w"> </span><span class="kd">defmodule</span><span class="w"> </span><span class="nc">MyApp.Accounts.Type1</span><span class="w"> </span><span class="k" data-group-id="4751465373-1">do</span><span class="w">
  </span><span class="kn">use</span><span class="w"> </span><span class="nc">Ecto.Schema</span><span class="w">

  </span><span class="kn">import</span><span class="w"> </span><span class="nc">Ecto.Changeset</span><span class="w">

  </span><span class="na">@primary_key</span><span class="w"> </span><span class="no">false</span><span class="w">

  </span><span class="n">embedded_schema</span><span class="w"> </span><span class="k" data-group-id="4751465373-2">do</span><span class="w">
    </span><span class="n">field</span><span class="w"> </span><span class="ss">:field_1</span><span class="p">,</span><span class="w"> </span><span class="ss">:string</span><span class="w">
  </span><span class="k" data-group-id="4751465373-2">end</span><span class="w">

  </span><span class="kd">def</span><span class="w"> </span><span class="nf">changeset</span><span class="p" data-group-id="4751465373-3">(</span><span class="n">changeset</span><span class="p">,</span><span class="w"> </span><span class="n">attrs</span><span class="p" data-group-id="4751465373-3">)</span><span class="w"> </span><span class="k" data-group-id="4751465373-4">do</span><span class="w">
    </span><span class="n">changeset</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">cast</span><span class="p" data-group-id="4751465373-5">(</span><span class="n">attrs</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="4751465373-6">[</span><span class="ss">:field_1</span><span class="p" data-group-id="4751465373-6">]</span><span class="p" data-group-id="4751465373-5">)</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">validate_required</span><span class="p" data-group-id="4751465373-7">(</span><span class="p" data-group-id="4751465373-8">[</span><span class="ss">:field_1</span><span class="p" data-group-id="4751465373-8">]</span><span class="p" data-group-id="4751465373-7">)</span><span class="w">
  </span><span class="k" data-group-id="4751465373-4">end</span><span class="w">
</span><span class="k" data-group-id="4751465373-1">end</span></code></pre>
<p>
This is a simplified example from the project I was working on. As you can see we want the metadata to be polymorphic, decided by the parent account. We’re going to build the <code class="inline">PolymorphicEmbed</code> Ecto type and its <code class="inline">cast_polymorphic_embed/3</code> function.</p>
<p>
First, we’ll build the <a href="https://hexdocs.pm/ecto/3.11.1/Ecto.ParameterizedType.html">parameterized type</a>:</p>
<pre><code class="makeup elixir"><span class="kd">defmodule</span><span class="w"> </span><span class="nc">MyApp.PolymorphicEmbed</span><span class="w"> </span><span class="k" data-group-id="2069813292-1">do</span><span class="w">
  </span><span class="na">@moduledoc</span><span class="w"> </span><span class="s">&quot;&quot;&quot;
  Polymorphic metadata is using Ecto&#39;s native embedded type.

  The data type is made polymorphic by passing in the schema module when
  casting the field.
  &quot;&quot;&quot;</span><span class="w">

  </span><span class="kd">defstruct</span><span class="w"> </span><span class="p" data-group-id="2069813292-2">[</span><span class="w">
    </span><span class="ss">:cardinality</span><span class="p">,</span><span class="w">
    </span><span class="ss">:related</span><span class="p">,</span><span class="w">
    </span><span class="ss">:on_cast</span><span class="w">
  </span><span class="p" data-group-id="2069813292-2">]</span><span class="w">

  </span><span class="kn">use</span><span class="w"> </span><span class="nc">Ecto.ParameterizedType</span><span class="w">

  </span><span class="kn">alias</span><span class="w"> </span><span class="bp">__MODULE__</span><span class="w">

  </span><span class="na">@impl</span><span class="w"> </span><span class="nc">Ecto.ParameterizedType</span><span class="w">
  </span><span class="kd">def</span><span class="w"> </span><span class="nf">type</span><span class="p" data-group-id="2069813292-3">(</span><span class="c">_params</span><span class="p" data-group-id="2069813292-3">)</span><span class="p">,</span><span class="w"> </span><span class="ss">do</span><span class="p">:</span><span class="w"> </span><span class="ss">:map</span><span class="w">

  </span><span class="na">@impl</span><span class="w"> </span><span class="nc">Ecto.ParameterizedType</span><span class="w">
  </span><span class="kd">def</span><span class="w"> </span><span class="nf">init</span><span class="p" data-group-id="2069813292-4">(</span><span class="n">opts</span><span class="p" data-group-id="2069813292-4">)</span><span class="w"> </span><span class="k" data-group-id="2069813292-5">do</span><span class="w">
    </span><span class="n">opts</span><span class="w"> </span><span class="o">=</span><span class="w">
      </span><span class="n">opts</span><span class="w">
      </span><span class="o">|&gt;</span><span class="w"> </span><span class="nc">Keyword</span><span class="o">.</span><span class="n">put_new</span><span class="p" data-group-id="2069813292-6">(</span><span class="ss">:on_replace</span><span class="p">,</span><span class="w"> </span><span class="ss">:raise</span><span class="p" data-group-id="2069813292-6">)</span><span class="w">
      </span><span class="o">|&gt;</span><span class="w"> </span><span class="nc">Keyword</span><span class="o">.</span><span class="n">put_new</span><span class="p" data-group-id="2069813292-7">(</span><span class="ss">:cardinality</span><span class="p">,</span><span class="w"> </span><span class="ss">:one</span><span class="p" data-group-id="2069813292-7">)</span><span class="w">

    </span><span class="n">struct</span><span class="p" data-group-id="2069813292-8">(</span><span class="p" data-group-id="2069813292-9">%</span><span class="nc" data-group-id="2069813292-9">PolymorphicEmbed</span><span class="p" data-group-id="2069813292-9">{</span><span class="p" data-group-id="2069813292-9">}</span><span class="p">,</span><span class="w"> </span><span class="n">opts</span><span class="p" data-group-id="2069813292-8">)</span><span class="w">
  </span><span class="k" data-group-id="2069813292-5">end</span><span class="w">

  </span><span class="na">@impl</span><span class="w"> </span><span class="nc">Ecto.ParameterizedType</span><span class="w">
  </span><span class="kd">defdelegate</span><span class="w"> </span><span class="n">load</span><span class="p" data-group-id="2069813292-10">(</span><span class="n">value</span><span class="p">,</span><span class="w"> </span><span class="n">fun</span><span class="p">,</span><span class="w"> </span><span class="n">opts</span><span class="p" data-group-id="2069813292-10">)</span><span class="p">,</span><span class="w"> </span><span class="ss">to</span><span class="p">:</span><span class="w"> </span><span class="nc">Ecto.Embedded</span><span class="w">

  </span><span class="na">@impl</span><span class="w"> </span><span class="nc">Ecto.ParameterizedType</span><span class="w">
  </span><span class="kd">defdelegate</span><span class="w"> </span><span class="n">cast</span><span class="p" data-group-id="2069813292-11">(</span><span class="n">data</span><span class="p">,</span><span class="w"> </span><span class="n">params</span><span class="p" data-group-id="2069813292-11">)</span><span class="p">,</span><span class="w"> </span><span class="ss">to</span><span class="p">:</span><span class="w"> </span><span class="nc">Ecto.Embedded</span><span class="w">

  </span><span class="na">@impl</span><span class="w"> </span><span class="nc">Ecto.ParameterizedType</span><span class="w">
  </span><span class="kd">defdelegate</span><span class="w"> </span><span class="n">dump</span><span class="p" data-group-id="2069813292-12">(</span><span class="n">value</span><span class="p">,</span><span class="w"> </span><span class="n">fun</span><span class="p">,</span><span class="w"> </span><span class="n">embed</span><span class="p" data-group-id="2069813292-12">)</span><span class="p">,</span><span class="w"> </span><span class="ss">to</span><span class="p">:</span><span class="w"> </span><span class="nc">Ecto.Embedded</span><span class="w">

  </span><span class="kn">alias</span><span class="w"> </span><span class="nc">Ecto.Changeset.Relation</span><span class="w">

  </span><span class="na">@behaviour</span><span class="w"> </span><span class="nc">Relation</span><span class="w">

  </span><span class="na">@impl</span><span class="w"> </span><span class="nc">Relation</span><span class="w">
  </span><span class="kd">def</span><span class="w"> </span><span class="nf">build</span><span class="p" data-group-id="2069813292-13">(</span><span class="p" data-group-id="2069813292-14">%</span><span class="nc" data-group-id="2069813292-14">PolymorphicEmbed</span><span class="p" data-group-id="2069813292-14">{</span><span class="ss">related</span><span class="p">:</span><span class="w"> </span><span class="n">related</span><span class="p" data-group-id="2069813292-14">}</span><span class="p">,</span><span class="w"> </span><span class="c">_owner</span><span class="p" data-group-id="2069813292-13">)</span><span class="w"> </span><span class="k" data-group-id="2069813292-15">do</span><span class="w">
    </span><span class="n">related</span><span class="o">.</span><span class="c">__struct__</span><span class="p" data-group-id="2069813292-16">(</span><span class="p" data-group-id="2069813292-16">)</span><span class="w">
  </span><span class="k" data-group-id="2069813292-15">end</span><span class="w">
</span><span class="k" data-group-id="2069813292-1">end</span></code></pre>
<p>
We want Ecto to handle this type just like a normal embed. The first part of this is done by creating a struct with <code class="inline">:cardinality</code>, <code class="inline">:related</code>, and <code class="inline">:on_cast</code> keys.</p>
<p>
One caveat with the above is that we won’t be able to use this in a database the same way as embeds. We’re just passing the load, cast, and dump functions straight through to <code class="inline">Ecto.Embedded</code>. There is not a way to load it back into the struct without either storing the struct somewhere, or always making sure we run it through the changeset. As I didn’t need to store this in the database I didn’t bother to make that work.</p>
<p>
Next up is adding the <code class="inline">cast_polymorphic_embed/3</code> function:</p>
<pre><code class="makeup elixir"><span class="kd">defmodule</span><span class="w"> </span><span class="nc">MyApp.PolymorphicEmbed</span><span class="w"> </span><span class="k" data-group-id="7466360558-1">do</span><span class="w">
  </span><span class="c1"># ...</span><span class="w">

  </span><span class="kn">alias</span><span class="w"> </span><span class="nc">Ecto.Changeset</span><span class="w">

  </span><span class="c1"># This is forcing an embed to be polymorphic by defining the</span><span class="w">
  </span><span class="c1"># schema module on runtime.</span><span class="w">
  </span><span class="kd">def</span><span class="w"> </span><span class="nf">cast_polymorphic_embed</span><span class="p" data-group-id="7466360558-2">(</span><span class="n">changeset</span><span class="p">,</span><span class="w"> </span><span class="n">related</span><span class="p">,</span><span class="w"> </span><span class="n">name</span><span class="p" data-group-id="7466360558-2">)</span><span class="w"> </span><span class="k" data-group-id="7466360558-3">do</span><span class="w">
    </span><span class="p" data-group-id="7466360558-4">%{</span><span class="ss">types</span><span class="p">:</span><span class="w"> </span><span class="n">types</span><span class="p" data-group-id="7466360558-4">}</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">changeset</span><span class="w">

    </span><span class="c1"># The related schema module will be set as the embedded type here.</span><span class="w">
    </span><span class="p" data-group-id="7466360558-5">{</span><span class="ss">:parameterized</span><span class="p">,</span><span class="w"> </span><span class="bp">__MODULE__</span><span class="p">,</span><span class="w"> </span><span class="n">relation</span><span class="p" data-group-id="7466360558-5">}</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">Map</span><span class="o">.</span><span class="n">fetch!</span><span class="p" data-group-id="7466360558-6">(</span><span class="n">types</span><span class="p">,</span><span class="w"> </span><span class="n">name</span><span class="p" data-group-id="7466360558-6">)</span><span class="w">
    </span><span class="n">relation</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p" data-group-id="7466360558-7">%{</span><span class="n">relation</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="ss">related</span><span class="p">:</span><span class="w"> </span><span class="n">related</span><span class="p" data-group-id="7466360558-7">}</span><span class="w">

    </span><span class="c1"># We must modify the type so Ecto will handle this field as an embed.</span><span class="w">
    </span><span class="n">changeset</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p" data-group-id="7466360558-8">%{</span><span class="n">changeset</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="ss">types</span><span class="p">:</span><span class="w"> </span><span class="nc">Map</span><span class="o">.</span><span class="n">put</span><span class="p" data-group-id="7466360558-9">(</span><span class="n">changeset</span><span class="o">.</span><span class="n">types</span><span class="p">,</span><span class="w"> </span><span class="n">name</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="7466360558-10">{</span><span class="ss">:embed</span><span class="p">,</span><span class="w"> </span><span class="n">relation</span><span class="p" data-group-id="7466360558-10">}</span><span class="p" data-group-id="7466360558-9">)</span><span class="p" data-group-id="7466360558-8">}</span><span class="w">

    </span><span class="nc">Changeset</span><span class="o">.</span><span class="n">cast_embed</span><span class="p" data-group-id="7466360558-11">(</span><span class="n">changeset</span><span class="p">,</span><span class="w"> </span><span class="n">name</span><span class="p" data-group-id="7466360558-11">)</span><span class="w">
  </span><span class="k" data-group-id="7466360558-3">end</span><span class="w">
</span><span class="k" data-group-id="7466360558-1">end</span></code></pre>
<p>
When Phoenix maps a changeset to a form it’ll use the type derived from the schema to figure out what keys exist. We’re manipulating the changeset types ensuring it looks like a regular hardcoded embed when casting the polymorphic embed.</p>
<p>
This is a double win since when we modify the type in the changeset we can also just depend on the <code class="inline">cast_embed/2</code> function to cast it.</p>
<p>
Now we can use it just like any embed in our form:</p>
<pre><code class="makeup elixir"><span class="o">&lt;</span><span class="o">.</span><span class="n">simple_form</span><span class="w"> </span><span class="k">for</span><span class="o">=</span><span class="p" data-group-id="4375236243-1">{</span><span class="na">@form</span><span class="p" data-group-id="4375236243-1">}</span><span class="o">&gt;</span><span class="w">
  </span><span class="o">&lt;</span><span class="o">.</span><span class="n">inputs_for</span><span class="w"> </span><span class="ss">:let</span><span class="o">=</span><span class="p" data-group-id="4375236243-2">{</span><span class="n">metadata_form</span><span class="p" data-group-id="4375236243-2">}</span><span class="w"> </span><span class="n">field</span><span class="o">=</span><span class="p" data-group-id="4375236243-3">{</span><span class="na">@form</span><span class="p" data-group-id="4375236243-4">[</span><span class="ss">:metadata</span><span class="p" data-group-id="4375236243-4">]</span><span class="p" data-group-id="4375236243-3">}</span><span class="o">&gt;</span><span class="w">
    </span><span class="o">&lt;</span><span class="o">.</span><span class="n">input</span><span class="w"> </span><span class="n">type</span><span class="o">=</span><span class="s">&quot;text&quot;</span><span class="w"> </span><span class="n">field</span><span class="o">=</span><span class="p" data-group-id="4375236243-5">{</span><span class="n">metadata_form</span><span class="p" data-group-id="4375236243-6">[</span><span class="ss">:field_1</span><span class="p" data-group-id="4375236243-6">]</span><span class="p" data-group-id="4375236243-5">}</span><span class="w"> </span><span class="n">label</span><span class="o">=</span><span class="s">&quot;Field 1&quot;</span><span class="w"> </span><span class="ss">:if</span><span class="o">=</span><span class="p" data-group-id="4375236243-7">{</span><span class="nc">Map</span><span class="o">.</span><span class="n">has_key?</span><span class="p" data-group-id="4375236243-8">(</span><span class="n">metadata_form</span><span class="o">.</span><span class="n">data</span><span class="p">,</span><span class="w"> </span><span class="ss">:field_1</span><span class="p" data-group-id="4375236243-8">)</span><span class="p" data-group-id="4375236243-7">}</span><span class="o">&gt;</span><span class="w"> </span><span class="o">/</span><span class="o">&gt;</span><span class="w">
  </span><span class="o">&lt;</span><span class="o">/</span><span class="o">.</span><span class="n">inputs_for</span><span class="o">&gt;</span><span class="w">
</span><span class="o">&lt;</span><span class="o">/</span><span class="o">.</span><span class="n">simple_form</span><span class="o">&gt;</span></code></pre>
<p>
The <code class="inline">:if</code> attribute is necessary as the metadata fields will differ. We only want to display the input field if it exists in the metadata schema.</p>
<p>
Any place you are going to use the changeset, remember to always run it through the <code class="inline">changeset/2</code> function containing the <code class="inline">cast_polymorphic_embed/3</code> otherwise the polymorphic embed will not be set.</p>

      ]]>
    </content>
  </entry>
  
  <entry>
    <title>How to set up AWS RDS IAM database authentication with Ecto</title>
    <link href="https://danschultzer.com/posts/setup-rds-iam-auth-with-ecto" />
    <id>https://danschultzer.com/posts/setup-rds-iam-auth-with-ecto</id>
    <updated>2023-11-28T00:00:00Z</updated>
    <summary>Snippets helping you set up AWS RDS IAM database authentication with Ecto.</summary>
    <content type="html">
      <![CDATA[
        <h2>
Using <code class="inline">AWS</code> (2024-03-24)</h2>
<blockquote>
  <p>
Recently I had to switch from <a href="https://github.com/ex-aws/ex_aws"><code class="inline">ExAWS</code></a> to <a href="https://github.com/aws-beam/aws-elixir"><code class="inline">AWS</code></a>. You can read how to set it up with <code class="inline">ExAWS</code> below.  </p>
</blockquote>
<p>
AWS RDS supports <a href="https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/UsingWithRDS.IAMDBAuth.html">IAM database authentication</a>. This means that we don’t have to deal with password rotation and can instead use shortlived tokens as database passwords!</p>
<p>
To set up RDS IAM database authentication you need to <a href="https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/UsingWithRDS.IAMDBAuth.Enabling.html">enable RDS IAM authentication</a> first and ensure that your database user has <a href="https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/UsingWithRDS.IAMDBAuth.DBAccounts.html">RDS IAM authentication enabled</a>.</p>
<p>
In postgres this requires running a <code class="inline">GRANT rds_iam TO REPLACE_WITH_DB_USERNAME;</code> query.</p>
<p>
The token must be generated and used as the database password each time Ecto sets up a connection as the token will only be valid for 15 minutes.</p>
<p>
First, add the dependencies:</p>
<pre><code class="makeup elixir"><span class="kd">defp</span><span class="w"> </span><span class="nf">deps</span><span class="w"> </span><span class="k" data-group-id="6769121442-1">do</span><span class="w">
  </span><span class="c1"># ...</span><span class="w">

  </span><span class="c1"># To generate the presigned url and for working with AWS</span><span class="w">
  </span><span class="p" data-group-id="6769121442-2">{</span><span class="ss">:aws</span><span class="p">,</span><span class="w"> </span><span class="s">&quot;~&gt; 0.14.0&quot;</span><span class="p" data-group-id="6769121442-2">}</span><span class="p">,</span><span class="w">

  </span><span class="c1"># Alternatively you can just include `:aws_signature` if you are not</span><span class="w">
  </span><span class="c1"># going to use `AWS` for anything.</span><span class="w">
  </span><span class="c1"># {:aws_signature, &quot;~&gt; 0.3&quot;}</span><span class="w">

  </span><span class="c1"># To fetch AWS credentials </span><span class="w">
  </span><span class="c1">#</span><span class="w">
  </span><span class="c1"># We don&#39;t want `:aws_credentials` to start in dev and test since</span><span class="w">
  </span><span class="c1"># the startup will fail if no credentials can be fetched.</span><span class="w">
  </span><span class="p" data-group-id="6769121442-3">{</span><span class="ss">:aws_credentials</span><span class="p">,</span><span class="w"> </span><span class="s">&quot;~&gt; 0.2.1&quot;</span><span class="p">,</span><span class="w"> </span><span class="ss">runtime</span><span class="p">:</span><span class="w"> </span><span class="nc">Mix</span><span class="o">.</span><span class="n">env</span><span class="p" data-group-id="6769121442-4">(</span><span class="p" data-group-id="6769121442-4">)</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="ss">:prod</span><span class="p" data-group-id="6769121442-3">}</span><span class="p">,</span><span class="w">

  </span><span class="c1"># CAStore for connecting to RDS with TLS</span><span class="w">
  </span><span class="p" data-group-id="6769121442-5">{</span><span class="ss">:aws_rds_castore</span><span class="p">,</span><span class="w"> </span><span class="s">&quot;~&gt; 1.1&quot;</span><span class="p" data-group-id="6769121442-5">}</span><span class="p">,</span><span class="w">
</span><span class="k" data-group-id="6769121442-1">end</span></code></pre>
<p>
We only start <code class="inline">:aws_credentials</code> in production as <code class="inline">:aws_credentials</code> halts the application startup if it can’t fetch any AWS credentials.</p>
<p>
Now set up <code class="inline">config/runtime.exs</code> to fetch the required arguments:</p>
<pre><code class="makeup elixir"><span class="n">config</span><span class="w"> </span><span class="ss">:my_app</span><span class="p">,</span><span class="w"> </span><span class="nc">MyApp.Repo</span><span class="p">,</span><span class="w">
  </span><span class="ss">configure</span><span class="p">:</span><span class="w"> </span><span class="p" data-group-id="3866364655-1">{</span><span class="w">
    </span><span class="nc">MyApp.Repo</span><span class="p">,</span><span class="w"> 
    </span><span class="ss">:configure_with_auth_token</span><span class="p">,</span><span class="w">
    </span><span class="p" data-group-id="3866364655-2">[</span><span class="w">
      </span><span class="ss">host</span><span class="p">:</span><span class="w"> </span><span class="n">host</span><span class="p">,</span><span class="w">
      </span><span class="ss">username</span><span class="p">:</span><span class="w"> </span><span class="n">username</span><span class="p">,</span><span class="w">
      </span><span class="ss">dbname</span><span class="p">:</span><span class="w"> </span><span class="n">dbname</span><span class="p">,</span><span class="w">
      </span><span class="ss">port</span><span class="p">:</span><span class="w"> </span><span class="n">port</span><span class="p">,</span><span class="w">
      </span><span class="ss">region</span><span class="p">:</span><span class="w"> </span><span class="n">region</span><span class="w">
    </span><span class="p" data-group-id="3866364655-2">]</span><span class="w">
  </span><span class="p" data-group-id="3866364655-1">}</span></code></pre>
<p>
I store these arguments in a single environment variable as JSON and decode it before setting them here.</p>
<p>
Also make sure <code class="inline">:aws_credentials</code> is started when running migrations in <code class="inline">config/runtime.exs</code>:</p>
<pre><code class="makeup elixir"><span class="n">start_apps_before_migration</span><span class="w"> </span><span class="o">=</span><span class="w">
  </span><span class="ss">:my_app</span><span class="w">
  </span><span class="o">|&gt;</span><span class="w"> </span><span class="nc">Application</span><span class="o">.</span><span class="n">fetch_env!</span><span class="p" data-group-id="9044464552-1">(</span><span class="nc">MyApp.Repo</span><span class="p" data-group-id="9044464552-1">)</span><span class="w">
  </span><span class="o">|&gt;</span><span class="w"> </span><span class="nc">Keyword</span><span class="o">.</span><span class="n">fetch!</span><span class="p" data-group-id="9044464552-2">(</span><span class="ss">:start_apps_before_migration</span><span class="p" data-group-id="9044464552-2">)</span><span class="w">
  </span><span class="o">|&gt;</span><span class="w"> </span><span class="nc">Kernel</span><span class="o">.</span><span class="o">++</span><span class="p" data-group-id="9044464552-3">(</span><span class="p" data-group-id="9044464552-4">[</span><span class="ss">:aws_credentials</span><span class="p" data-group-id="9044464552-4">]</span><span class="p" data-group-id="9044464552-3">)</span><span class="w">

</span><span class="n">config</span><span class="w"> </span><span class="ss">:my_app</span><span class="p">,</span><span class="w"> </span><span class="nc">MyApp.Repo</span><span class="p">,</span><span class="w"> </span><span class="ss">start_apps_before_migration</span><span class="p">:</span><span class="w"> </span><span class="n">start_apps_before_migration</span></code></pre>
<p>
We put this in <code class="inline">config/runtime.exs</code> as <code class="inline">:aws_credentials</code> will fail startup when it can’t fetch the AWS credentials. We don’t want it to run in the development and test environments.</p>
<p>
Finally set up <code class="inline">configure_with_auth_token/2</code> in your repo module:</p>
<pre><code class="makeup elixir"><span class="kd">defmodule</span><span class="w"> </span><span class="nc">MyApp.Repo</span><span class="w"> </span><span class="k" data-group-id="0016143872-1">do</span><span class="w">
  </span><span class="kn">use</span><span class="w"> </span><span class="nc">Ecto.Repo</span><span class="p">,</span><span class="w">
    </span><span class="ss">otp_app</span><span class="p">:</span><span class="w"> </span><span class="ss">:my_app</span><span class="p">,</span><span class="w">
    </span><span class="ss">adapter</span><span class="p">:</span><span class="w"> </span><span class="nc">Ecto.Adapters.Postgres</span><span class="w">

  </span><span class="c1"># Helper function to configure the connection with dynamically generated</span><span class="w">
  </span><span class="c1"># auth token for the IAM instance role</span><span class="w">
  </span><span class="kd">def</span><span class="w"> </span><span class="nf">configure_with_auth_token</span><span class="p" data-group-id="0016143872-2">(</span><span class="n">opts</span><span class="p">,</span><span class="w"> </span><span class="n">credentials</span><span class="p" data-group-id="0016143872-2">)</span><span class="w"> </span><span class="k" data-group-id="0016143872-3">do</span><span class="w">
    </span><span class="n">hostname</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">Keyword</span><span class="o">.</span><span class="n">fetch!</span><span class="p" data-group-id="0016143872-4">(</span><span class="n">credentials</span><span class="p">,</span><span class="w"> </span><span class="ss">:host</span><span class="p" data-group-id="0016143872-4">)</span><span class="w">
    </span><span class="n">username</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">Keyword</span><span class="o">.</span><span class="n">fetch!</span><span class="p" data-group-id="0016143872-5">(</span><span class="n">credentials</span><span class="p">,</span><span class="w"> </span><span class="ss">:username</span><span class="p" data-group-id="0016143872-5">)</span><span class="w">
    </span><span class="n">port</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">Keyword</span><span class="o">.</span><span class="n">fetch!</span><span class="p" data-group-id="0016143872-6">(</span><span class="n">credentials</span><span class="p">,</span><span class="w"> </span><span class="ss">:port</span><span class="p" data-group-id="0016143872-6">)</span><span class="w">
    </span><span class="n">dbname</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">Keyword</span><span class="o">.</span><span class="n">fetch!</span><span class="p" data-group-id="0016143872-7">(</span><span class="n">credentials</span><span class="p">,</span><span class="w"> </span><span class="ss">:dbname</span><span class="p" data-group-id="0016143872-7">)</span><span class="w">
    </span><span class="n">region</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">Keyword</span><span class="o">.</span><span class="n">fetch!</span><span class="p" data-group-id="0016143872-8">(</span><span class="n">credentials</span><span class="p">,</span><span class="w"> </span><span class="ss">:region</span><span class="p" data-group-id="0016143872-8">)</span><span class="w">

    </span><span class="n">aws_credentials</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">:aws_credentials</span><span class="o">.</span><span class="n">get_credentials</span><span class="p" data-group-id="0016143872-9">(</span><span class="p" data-group-id="0016143872-9">)</span><span class="w">
    </span><span class="n">auth_token</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">rds_auth_token</span><span class="p" data-group-id="0016143872-10">(</span><span class="n">aws_credentials</span><span class="p">,</span><span class="w"> </span><span class="n">hostname</span><span class="p">,</span><span class="w"> </span><span class="n">port</span><span class="p">,</span><span class="w"> </span><span class="n">username</span><span class="p">,</span><span class="w"> </span><span class="n">region</span><span class="p" data-group-id="0016143872-10">)</span><span class="w">

    </span><span class="nc">Keyword</span><span class="o">.</span><span class="n">merge</span><span class="p" data-group-id="0016143872-11">(</span><span class="n">opts</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="0016143872-12">[</span><span class="w">
      </span><span class="ss">hostname</span><span class="p">:</span><span class="w"> </span><span class="n">hostname</span><span class="p">,</span><span class="w">
      </span><span class="ss">port</span><span class="p">:</span><span class="w"> </span><span class="n">port</span><span class="p">,</span><span class="w">
      </span><span class="ss">username</span><span class="p">:</span><span class="w"> </span><span class="n">username</span><span class="p">,</span><span class="w">
      </span><span class="ss">password</span><span class="p">:</span><span class="w"> </span><span class="n">auth_token</span><span class="p">,</span><span class="w">
      </span><span class="ss">database</span><span class="p">:</span><span class="w"> </span><span class="n">dbname</span><span class="p">,</span><span class="w">
      </span><span class="ss">ssl</span><span class="p">:</span><span class="w"> </span><span class="no">true</span><span class="p">,</span><span class="w">
      </span><span class="ss">ssl_opts</span><span class="p">:</span><span class="w"> </span><span class="nc">AwsRdsCAStore</span><span class="o">.</span><span class="n">ssl_opts</span><span class="p" data-group-id="0016143872-13">(</span><span class="n">hostname</span><span class="p" data-group-id="0016143872-13">)</span><span class="w">
    </span><span class="p" data-group-id="0016143872-12">]</span><span class="p" data-group-id="0016143872-11">)</span><span class="w">
  </span><span class="k" data-group-id="0016143872-3">end</span><span class="w">

  </span><span class="kd">defp</span><span class="w"> </span><span class="nf">rds_auth_token</span><span class="p" data-group-id="0016143872-14">(</span><span class="n">aws_credentials</span><span class="p">,</span><span class="w"> </span><span class="n">hostname</span><span class="p">,</span><span class="w"> </span><span class="n">port</span><span class="p">,</span><span class="w"> </span><span class="n">username</span><span class="p">,</span><span class="w"> </span><span class="n">region</span><span class="p">,</span><span class="w"> </span><span class="n">opts</span><span class="w"> </span><span class="o">\\</span><span class="w"> </span><span class="p" data-group-id="0016143872-15">[</span><span class="ss">ttl</span><span class="p">:</span><span class="w"> </span><span class="mi">900</span><span class="p" data-group-id="0016143872-15">]</span><span class="p" data-group-id="0016143872-14">)</span><span class="w"> </span><span class="k" data-group-id="0016143872-16">do</span><span class="w">
    </span><span class="n">access_key</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">Map</span><span class="o">.</span><span class="n">fetch!</span><span class="p" data-group-id="0016143872-17">(</span><span class="n">aws_credentials</span><span class="p">,</span><span class="w"> </span><span class="ss">:access_key_id</span><span class="p" data-group-id="0016143872-17">)</span><span class="w">
    </span><span class="n">secret_key</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">Map</span><span class="o">.</span><span class="n">fetch!</span><span class="p" data-group-id="0016143872-18">(</span><span class="n">aws_credentials</span><span class="p">,</span><span class="w"> </span><span class="ss">:secret_access_key</span><span class="p" data-group-id="0016143872-18">)</span><span class="w">
    </span><span class="n">datetime</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">:erlang</span><span class="o">.</span><span class="n">universaltime</span><span class="p" data-group-id="0016143872-19">(</span><span class="p" data-group-id="0016143872-19">)</span><span class="w">
    </span><span class="n">url</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s">&quot;https://</span><span class="si" data-group-id="0016143872-20">#{</span><span class="n">hostname</span><span class="si" data-group-id="0016143872-20">}</span><span class="s">:</span><span class="si" data-group-id="0016143872-21">#{</span><span class="n">port</span><span class="si" data-group-id="0016143872-21">}</span><span class="s">/?Action=connect&amp;DBUser=</span><span class="si" data-group-id="0016143872-22">#{</span><span class="n">username</span><span class="si" data-group-id="0016143872-22">}</span><span class="s">&quot;</span><span class="w">

    </span><span class="n">signed_url</span><span class="w"> </span><span class="o">=</span><span class="w">
      </span><span class="nc">:aws_signature</span><span class="o">.</span><span class="n">sign_v4_query_params</span><span class="p" data-group-id="0016143872-23">(</span><span class="w">
        </span><span class="n">access_key</span><span class="p">,</span><span class="w">
        </span><span class="n">secret_key</span><span class="p">,</span><span class="w">
        </span><span class="n">region</span><span class="p">,</span><span class="w">
        </span><span class="s">&quot;rds-db&quot;</span><span class="p">,</span><span class="w">
        </span><span class="n">datetime</span><span class="p">,</span><span class="w">
        </span><span class="n">url</span><span class="p">,</span><span class="w">
        </span><span class="n">opts</span><span class="w">
      </span><span class="p" data-group-id="0016143872-23">)</span><span class="w">

    </span><span class="nc">String</span><span class="o">.</span><span class="n">trim_leading</span><span class="p" data-group-id="0016143872-24">(</span><span class="n">signed_url</span><span class="p">,</span><span class="w"> </span><span class="s">&quot;https://&quot;</span><span class="p" data-group-id="0016143872-24">)</span><span class="w">
  </span><span class="k" data-group-id="0016143872-16">end</span><span class="w">
</span><span class="k" data-group-id="0016143872-1">end</span></code></pre>
<p>
I still had to keep the <code class="inline">wait_for_connection</code> logic in place from further down to prevent flakey deployments.</p>
<h2>
Assuming role</h2>
<p>
To connect to a database cross-account you must assume a role before generating the token. This wasn’t too bad using <code class="inline">AWS</code>, but did take a minute to get right. Instead of calling <code class="inline">:aws_credentials.get_credentials/0</code> in <code class="inline">MyApp.Repo.configure_with_auth_token/2</code>, we’ll assume the role first:</p>
<pre><code class="makeup elixir"><span class="kd">defmodule</span><span class="w"> </span><span class="nc">MyApp.Repo</span><span class="w"> </span><span class="k" data-group-id="6898260801-1">do</span><span class="w">
  </span><span class="c1"># ...</span><span class="w">
  </span><span class="kd">defp</span><span class="w"> </span><span class="nf">get_aws_credentials</span><span class="p" data-group-id="6898260801-2">(</span><span class="n">db_credentials</span><span class="p">,</span><span class="w"> </span><span class="n">region</span><span class="p" data-group-id="6898260801-2">)</span><span class="w"> </span><span class="k" data-group-id="6898260801-3">do</span><span class="w">
    </span><span class="n">aws_credentials</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">:aws_credentials</span><span class="o">.</span><span class="n">get_credentials</span><span class="p" data-group-id="6898260801-4">(</span><span class="p" data-group-id="6898260801-4">)</span><span class="w">

    </span><span class="k">case</span><span class="w"> </span><span class="nc">Keyword</span><span class="o">.</span><span class="n">get</span><span class="p" data-group-id="6898260801-5">(</span><span class="n">db_credentials</span><span class="p">,</span><span class="w"> </span><span class="ss">:role_arn</span><span class="p" data-group-id="6898260801-5">)</span><span class="w"> </span><span class="k" data-group-id="6898260801-6">do</span><span class="w">
      </span><span class="no">nil</span><span class="w"> </span><span class="o">-&gt;</span><span class="w"> </span><span class="n">aws_credentials</span><span class="w">
      </span><span class="n">role_arn</span><span class="w"> </span><span class="o">-&gt;</span><span class="w"> </span><span class="n">assume_role</span><span class="p" data-group-id="6898260801-7">(</span><span class="n">aws_credentials</span><span class="p">,</span><span class="w"> </span><span class="n">region</span><span class="p">,</span><span class="w"> </span><span class="n">role_arn</span><span class="p" data-group-id="6898260801-7">)</span><span class="w">
    </span><span class="k" data-group-id="6898260801-6">end</span><span class="w">
  </span><span class="k" data-group-id="6898260801-3">end</span><span class="w">

  </span><span class="kd">defp</span><span class="w"> </span><span class="nf">assume_role</span><span class="p" data-group-id="6898260801-8">(</span><span class="n">aws_credentials</span><span class="p">,</span><span class="w"> </span><span class="n">region</span><span class="p">,</span><span class="w"> </span><span class="n">role_arn</span><span class="p">,</span><span class="w"> </span><span class="n">opts</span><span class="w"> </span><span class="o">\\</span><span class="w"> </span><span class="p" data-group-id="6898260801-9">[</span><span class="ss">ttl</span><span class="p">:</span><span class="w"> </span><span class="mi">900</span><span class="p" data-group-id="6898260801-9">]</span><span class="p" data-group-id="6898260801-8">)</span><span class="w"> </span><span class="k" data-group-id="6898260801-10">do</span><span class="w">
    </span><span class="n">access_key</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">Map</span><span class="o">.</span><span class="n">fetch!</span><span class="p" data-group-id="6898260801-11">(</span><span class="n">aws_credentials</span><span class="p">,</span><span class="w"> </span><span class="ss">:access_key_id</span><span class="p" data-group-id="6898260801-11">)</span><span class="w">
    </span><span class="n">secret_key</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">Map</span><span class="o">.</span><span class="n">fetch!</span><span class="p" data-group-id="6898260801-12">(</span><span class="n">aws_credentials</span><span class="p">,</span><span class="w"> </span><span class="ss">:secret_access_key</span><span class="p" data-group-id="6898260801-12">)</span><span class="w">
    </span><span class="n">session_token</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">Map</span><span class="o">.</span><span class="n">fetch!</span><span class="p" data-group-id="6898260801-13">(</span><span class="n">aws_credentials</span><span class="p">,</span><span class="w"> </span><span class="ss">:token</span><span class="p" data-group-id="6898260801-13">)</span><span class="w">
    </span><span class="n">client</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">AWS.Client</span><span class="o">.</span><span class="n">create</span><span class="p" data-group-id="6898260801-14">(</span><span class="n">access_key</span><span class="p">,</span><span class="w"> </span><span class="n">secret_key</span><span class="p">,</span><span class="w"> </span><span class="n">session_token</span><span class="p">,</span><span class="w"> </span><span class="n">region</span><span class="p" data-group-id="6898260801-14">)</span><span class="w">

    </span><span class="n">input</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p" data-group-id="6898260801-15">%{</span><span class="w">
      </span><span class="s">&quot;DurationSeconds&quot;</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="nc">Map</span><span class="o">.</span><span class="n">fetch!</span><span class="p" data-group-id="6898260801-16">(</span><span class="n">opts</span><span class="p">,</span><span class="w"> </span><span class="ss">:ttl</span><span class="p" data-group-id="6898260801-16">)</span><span class="p">,</span><span class="w">
      </span><span class="s">&quot;RoleArn&quot;</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="n">role_arn</span><span class="p">,</span><span class="w">
      </span><span class="s">&quot;RoleSessionName&quot;</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="n">to_string</span><span class="p" data-group-id="6898260801-17">(</span><span class="bp">__MODULE__</span><span class="p" data-group-id="6898260801-17">)</span><span class="w">
    </span><span class="p" data-group-id="6898260801-15">}</span><span class="w">

    </span><span class="k">case</span><span class="w"> </span><span class="nc">AWS.STS</span><span class="o">.</span><span class="n">assume_role</span><span class="p" data-group-id="6898260801-18">(</span><span class="n">client</span><span class="p">,</span><span class="w"> </span><span class="n">input</span><span class="p" data-group-id="6898260801-18">)</span><span class="w"> </span><span class="k" data-group-id="6898260801-19">do</span><span class="w">
      </span><span class="p" data-group-id="6898260801-20">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="6898260801-21">%{</span><span class="w">
        </span><span class="s">&quot;AssumeRoleResponse&quot;</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="p" data-group-id="6898260801-22">%{</span><span class="w">
          </span><span class="s">&quot;AssumeRoleResult&quot;</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="p" data-group-id="6898260801-23">%{</span><span class="w">
            </span><span class="s">&quot;Credentials&quot;</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="p" data-group-id="6898260801-24">%{</span><span class="w">
              </span><span class="s">&quot;AccessKeyId&quot;</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="n">access_key_id</span><span class="p">,</span><span class="w">
              </span><span class="s">&quot;SecretAccessKey&quot;</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="n">secret_access_key</span><span class="p">,</span><span class="w">
              </span><span class="s">&quot;SessionToken&quot;</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="n">session_token</span><span class="w">
            </span><span class="p" data-group-id="6898260801-24">}</span><span class="w">
          </span><span class="p" data-group-id="6898260801-23">}</span><span class="w">
        </span><span class="p" data-group-id="6898260801-22">}</span><span class="w">
      </span><span class="p" data-group-id="6898260801-21">}</span><span class="p">,</span><span class="w"> </span><span class="c">_response</span><span class="p" data-group-id="6898260801-20">}</span><span class="w"> </span><span class="o">-&gt;</span><span class="w">
        </span><span class="p" data-group-id="6898260801-25">%{</span><span class="w">
          </span><span class="ss">access_key_id</span><span class="p">:</span><span class="w"> </span><span class="n">access_key_id</span><span class="p">,</span><span class="w">
          </span><span class="ss">secret_access_key</span><span class="p">:</span><span class="w"> </span><span class="n">secret_access_key</span><span class="p">,</span><span class="w">
          </span><span class="ss">token</span><span class="p">:</span><span class="w"> </span><span class="n">session_token</span><span class="w">
        </span><span class="p" data-group-id="6898260801-25">}</span><span class="w">

      </span><span class="p" data-group-id="6898260801-26">{</span><span class="ss">:error</span><span class="p">,</span><span class="w"> </span><span class="n">error</span><span class="p" data-group-id="6898260801-26">}</span><span class="w"> </span><span class="o">-&gt;</span><span class="w">
        </span><span class="k">raise</span><span class="w"> </span><span class="n">error</span><span class="w">
    </span><span class="k" data-group-id="6898260801-19">end</span><span class="w">
  </span><span class="k" data-group-id="6898260801-10">end</span><span class="w">
</span><span class="k" data-group-id="6898260801-1">end</span></code></pre>
<p>
Now in <code class="inline">rds_auth_token/6</code> you must pull out the session token as we:</p>
<pre><code class="makeup elixir"><span class="kd">defp</span><span class="w"> </span><span class="nf">rds_auth_token</span><span class="p" data-group-id="7454381198-1">(</span><span class="n">aws_credentials</span><span class="p">,</span><span class="w"> </span><span class="n">hostname</span><span class="p">,</span><span class="w"> </span><span class="n">port</span><span class="p">,</span><span class="w"> </span><span class="n">username</span><span class="p">,</span><span class="w"> </span><span class="n">region</span><span class="p">,</span><span class="w"> </span><span class="n">opts</span><span class="w"> </span><span class="o">\\</span><span class="w"> </span><span class="p" data-group-id="7454381198-2">[</span><span class="ss">ttl</span><span class="p">:</span><span class="w"> </span><span class="mi">900</span><span class="p" data-group-id="7454381198-2">]</span><span class="p" data-group-id="7454381198-1">)</span><span class="w"> </span><span class="k" data-group-id="7454381198-3">do</span><span class="w">
  </span><span class="c1"># ...</span><span class="w">
  </span><span class="n">session_token</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">:uri_string</span><span class="o">.</span><span class="n">quote</span><span class="p" data-group-id="7454381198-4">(</span><span class="nc">Map</span><span class="o">.</span><span class="n">fetch!</span><span class="p" data-group-id="7454381198-5">(</span><span class="n">aws_credentials</span><span class="p">,</span><span class="w"> </span><span class="ss">:token</span><span class="p" data-group-id="7454381198-5">)</span><span class="p" data-group-id="7454381198-4">)</span><span class="w">
  </span><span class="n">opts</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">Keyword</span><span class="o">.</span><span class="n">put</span><span class="p" data-group-id="7454381198-6">(</span><span class="n">opts</span><span class="p">,</span><span class="w"> </span><span class="ss">:session_token</span><span class="p">,</span><span class="w"> </span><span class="n">session_token</span><span class="p" data-group-id="7454381198-6">)</span><span class="w">
  </span><span class="c1"># ...</span><span class="w">
</span><span class="k" data-group-id="7454381198-3">end</span></code></pre>
<p>
Remember to add <code class="inline">:role_arn</code> to your <code class="inline">MyApp.Repo</code> config.</p>
<h2>
Logging startup errors</h2>
<p>
Any exception raised in the <code class="inline">configure_with_auth_token/4</code> callback didn’t show up in the logs. To understand what went wrong when the app starts to fail, I had catch any exceptions and print them before reraising to halt the application startup:</p>
<pre><code class="makeup elixir"><span class="kd">defmodule</span><span class="w"> </span><span class="nc">MyApp.Repo</span><span class="w"> </span><span class="k" data-group-id="7466275153-1">do</span><span class="w">
  </span><span class="kd">def</span><span class="w"> </span><span class="nf">configure_with_auth_token</span><span class="p" data-group-id="7466275153-2">(</span><span class="n">opts</span><span class="p">,</span><span class="w"> </span><span class="n">credentials</span><span class="p" data-group-id="7466275153-2">)</span><span class="w"> </span><span class="k" data-group-id="7466275153-3">do</span><span class="w">
    </span><span class="c1"># ...</span><span class="w">
  </span><span class="k" data-group-id="7466275153-3">rescue</span><span class="w">
    </span><span class="n">error</span><span class="w"> </span><span class="o">-&gt;</span><span class="w">
      </span><span class="c1"># If there are any issues with starting the supervisor the whole app</span><span class="w">
      </span><span class="c1"># will be shut down, so we want to print early here.</span><span class="w">
      </span><span class="nc">IO</span><span class="o">.</span><span class="n">warn</span><span class="p" data-group-id="7466275153-4">(</span><span class="nc">Exception</span><span class="o">.</span><span class="n">format_banner</span><span class="p" data-group-id="7466275153-5">(</span><span class="ss">:error</span><span class="p">,</span><span class="w"> </span><span class="n">error</span><span class="p">,</span><span class="w"> </span><span class="c">__STACKTRACE__</span><span class="p" data-group-id="7466275153-5">)</span><span class="p" data-group-id="7466275153-4">)</span><span class="w">

      </span><span class="n">reraise</span><span class="w"> </span><span class="n">error</span><span class="p">,</span><span class="w"> </span><span class="c">__STACKTRACE__</span><span class="w">
  </span><span class="k" data-group-id="7466275153-3">end</span><span class="w">
</span><span class="k" data-group-id="7466275153-1">end</span></code></pre>
<h2>
Flakey deployments during migrations</h2>
<p>
I saw flakey deployments when I first implemented this in my ECS cluster. Migrations were running in an ECS task and frequently failed after a few seconds. There were no helpful error messages in the log, all it explained was:</p>
<pre><code class="log">[error] Could not create schema migrations table. This error usually happens due to the following:
...
** (DBConnection.ConnectionError) connection not available and request was dropped from queue after 2967ms. This means requests are coming in and your connection pool cannot serve them fast enough. You can address this by:
...</code></pre>
<p>
I found that this was a combination of both <code class="inline">ExAWS</code> and the <code class="inline">Ecto.Migrator.run/3</code> call. <code class="inline">ExAWS</code> takes a bit before it has the instance credentials available in the <code class="inline">ExAWS.Config.AuthCache</code> GenServer, and <code class="inline">Ecto.Migrator.run/3</code> won’t wait for a connection to be established with the <code class="inline">:configure</code> callback before running the query.</p>
<p>
What we have to do is to force the migration task to wait until we have an established connection. We could increase <code class="inline">:queue_target</code> and <code class="inline">:queue_interval</code>, but I felt it was better to just wait until the connection had been established.</p>
<p>
First, we’ll update our release module to call <code class="inline">wait_for_connection/1</code>:</p>
<pre><code class="makeup elixir"><span class="kd">defmodule</span><span class="w"> </span><span class="nc">MyApp.Release</span><span class="w"> </span><span class="k" data-group-id="7070766747-1">do</span><span class="w">
  </span><span class="c1"># ...</span><span class="w">

  </span><span class="kd">def</span><span class="w"> </span><span class="nf">migrate</span><span class="w"> </span><span class="k" data-group-id="7070766747-2">do</span><span class="w">
    </span><span class="n">load_app</span><span class="p" data-group-id="7070766747-3">(</span><span class="p" data-group-id="7070766747-3">)</span><span class="w">

    </span><span class="k">for</span><span class="w"> </span><span class="n">repo</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="n">repos</span><span class="p" data-group-id="7070766747-4">(</span><span class="p" data-group-id="7070766747-4">)</span><span class="w"> </span><span class="k" data-group-id="7070766747-5">do</span><span class="w">
      </span><span class="p" data-group-id="7070766747-6">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="bp">_</span><span class="p">,</span><span class="w"> </span><span class="bp">_</span><span class="p" data-group-id="7070766747-6">}</span><span class="w"> </span><span class="o">=</span><span class="w">
        </span><span class="nc">Ecto.Migrator</span><span class="o">.</span><span class="n">with_repo</span><span class="p" data-group-id="7070766747-7">(</span><span class="n">repo</span><span class="p">,</span><span class="w"> </span><span class="k" data-group-id="7070766747-8">fn</span><span class="w"> </span><span class="n">repo</span><span class="w"> </span><span class="o">-&gt;</span><span class="w">
          </span><span class="n">wait_for_connection</span><span class="p" data-group-id="7070766747-9">(</span><span class="n">repo</span><span class="p" data-group-id="7070766747-9">)</span><span class="w">
          </span><span class="nc">Ecto.Migrator</span><span class="o">.</span><span class="n">run</span><span class="p" data-group-id="7070766747-10">(</span><span class="n">repo</span><span class="p">,</span><span class="w"> </span><span class="ss">:up</span><span class="p">,</span><span class="w"> </span><span class="ss">all</span><span class="p">:</span><span class="w"> </span><span class="no">true</span><span class="p" data-group-id="7070766747-10">)</span><span class="w">
        </span><span class="k" data-group-id="7070766747-8">end</span><span class="p" data-group-id="7070766747-7">)</span><span class="w">
    </span><span class="k" data-group-id="7070766747-5">end</span><span class="w">
  </span><span class="k" data-group-id="7070766747-2">end</span><span class="w">

  </span><span class="kd">def</span><span class="w"> </span><span class="nf">rollback</span><span class="p" data-group-id="7070766747-11">(</span><span class="n">repo</span><span class="p">,</span><span class="w"> </span><span class="n">version</span><span class="p" data-group-id="7070766747-11">)</span><span class="w"> </span><span class="k" data-group-id="7070766747-12">do</span><span class="w">
    </span><span class="n">load_app</span><span class="p" data-group-id="7070766747-13">(</span><span class="p" data-group-id="7070766747-13">)</span><span class="w">

    </span><span class="p" data-group-id="7070766747-14">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="bp">_</span><span class="p">,</span><span class="w"> </span><span class="bp">_</span><span class="p" data-group-id="7070766747-14">}</span><span class="w"> </span><span class="o">=</span><span class="w">
      </span><span class="nc">Ecto.Migrator</span><span class="o">.</span><span class="n">with_repo</span><span class="p" data-group-id="7070766747-15">(</span><span class="n">repo</span><span class="p">,</span><span class="w"> </span><span class="k" data-group-id="7070766747-16">fn</span><span class="w"> </span><span class="n">repo</span><span class="w"> </span><span class="o">-&gt;</span><span class="w">
        </span><span class="n">wait_for_connection</span><span class="p" data-group-id="7070766747-17">(</span><span class="n">repo</span><span class="p" data-group-id="7070766747-17">)</span><span class="w">
        </span><span class="nc">Ecto.Migrator</span><span class="o">.</span><span class="n">run</span><span class="p" data-group-id="7070766747-18">(</span><span class="n">repo</span><span class="p">,</span><span class="w"> </span><span class="ss">:down</span><span class="p">,</span><span class="w"> </span><span class="ss">to</span><span class="p">:</span><span class="w"> </span><span class="n">version</span><span class="p" data-group-id="7070766747-18">)</span><span class="w">
      </span><span class="k" data-group-id="7070766747-16">end</span><span class="p" data-group-id="7070766747-15">)</span><span class="w">
  </span><span class="k" data-group-id="7070766747-12">end</span><span class="w">

  </span><span class="c1"># ...</span><span class="w">
</span><span class="k" data-group-id="7070766747-1">end</span></code></pre>
<p>
Now we implement <code class="inline">wait_for_connection/1</code> that will run a query every interval (every 50ms) until it gets a connection or times out (after 30s):</p>
<pre><code class="makeup elixir"><span class="kd">defmodule</span><span class="w"> </span><span class="nc">MyApp.Release</span><span class="w"> </span><span class="k" data-group-id="3991181523-1">do</span><span class="w">
  </span><span class="c1"># ...</span><span class="w">

  </span><span class="na">@interval</span><span class="w"> </span><span class="mi">50</span><span class="w">
  </span><span class="na">@timeout</span><span class="w"> </span><span class="nc">:timer</span><span class="o">.</span><span class="n">seconds</span><span class="p" data-group-id="3991181523-2">(</span><span class="mi">30</span><span class="p" data-group-id="3991181523-2">)</span><span class="w">

  </span><span class="c1"># Due to ExAWS being slow we want to give the migration task enough time</span><span class="w">
  </span><span class="c1"># to establish a connection before running the migrations.</span><span class="w">
  </span><span class="kd">defp</span><span class="w"> </span><span class="nf">wait_for_connection</span><span class="p" data-group-id="3991181523-3">(</span><span class="n">repo</span><span class="p" data-group-id="3991181523-3">)</span><span class="w"> </span><span class="k" data-group-id="3991181523-4">do</span><span class="w">
    </span><span class="n">wait_for_connection</span><span class="p" data-group-id="3991181523-5">(</span><span class="n">repo</span><span class="p">,</span><span class="w"> </span><span class="nc">System</span><span class="o">.</span><span class="n">monotonic_time</span><span class="p" data-group-id="3991181523-6">(</span><span class="p" data-group-id="3991181523-6">)</span><span class="p">,</span><span class="w"> </span><span class="mi">0</span><span class="p" data-group-id="3991181523-5">)</span><span class="w">
  </span><span class="k" data-group-id="3991181523-4">end</span><span class="w">

  </span><span class="kd">defp</span><span class="w"> </span><span class="nf">wait_for_connection</span><span class="p" data-group-id="3991181523-7">(</span><span class="n">repo</span><span class="p">,</span><span class="w"> </span><span class="c">_start</span><span class="p">,</span><span class="w"> </span><span class="n">time</span><span class="p" data-group-id="3991181523-7">)</span><span class="w"> </span><span class="ow">when</span><span class="w"> </span><span class="n">time</span><span class="w"> </span><span class="o">&gt;=</span><span class="w"> </span><span class="na">@timeout</span><span class="w"> </span><span class="k" data-group-id="3991181523-8">do</span><span class="w">
    </span><span class="k">raise</span><span class="w"> </span><span class="s">&quot;Could not establish a connection with </span><span class="si" data-group-id="3991181523-9">#{</span><span class="n">inspect</span><span class="w"> </span><span class="n">repo</span><span class="si" data-group-id="3991181523-9">}</span><span class="s"> after </span><span class="si" data-group-id="3991181523-10">#{</span><span class="n">time</span><span class="si" data-group-id="3991181523-10">}</span><span class="s">ms&quot;</span><span class="w">
  </span><span class="k" data-group-id="3991181523-8">end</span><span class="w">

  </span><span class="kd">defp</span><span class="w"> </span><span class="nf">wait_for_connection</span><span class="p" data-group-id="3991181523-11">(</span><span class="n">repo</span><span class="p">,</span><span class="w"> </span><span class="n">start</span><span class="p">,</span><span class="w"> </span><span class="c">_time</span><span class="p" data-group-id="3991181523-11">)</span><span class="w"> </span><span class="k" data-group-id="3991181523-12">do</span><span class="w">
    </span><span class="k">case</span><span class="w"> </span><span class="n">canary</span><span class="p" data-group-id="3991181523-13">(</span><span class="n">repo</span><span class="p" data-group-id="3991181523-13">)</span><span class="w"> </span><span class="k" data-group-id="3991181523-14">do</span><span class="w">
      </span><span class="ss">:ok</span><span class="w"> </span><span class="o">-&gt;</span><span class="w">
        </span><span class="ss">:ok</span><span class="w">

      </span><span class="ss">:error</span><span class="w"> </span><span class="o">-&gt;</span><span class="w">
        </span><span class="n">stop</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">System</span><span class="o">.</span><span class="n">monotonic_time</span><span class="p" data-group-id="3991181523-15">(</span><span class="p" data-group-id="3991181523-15">)</span><span class="w">
        </span><span class="n">time</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">System</span><span class="o">.</span><span class="n">convert_time_unit</span><span class="p" data-group-id="3991181523-16">(</span><span class="n">stop</span><span class="w"> </span><span class="o">-</span><span class="w"> </span><span class="n">start</span><span class="p">,</span><span class="w"> </span><span class="ss">:native</span><span class="p">,</span><span class="w"> </span><span class="ss">:millisecond</span><span class="p" data-group-id="3991181523-16">)</span><span class="w">

        </span><span class="nc">:timer</span><span class="o">.</span><span class="n">sleep</span><span class="p" data-group-id="3991181523-17">(</span><span class="na">@interval</span><span class="p" data-group-id="3991181523-17">)</span><span class="w">
        </span><span class="n">wait_for_connection</span><span class="p" data-group-id="3991181523-18">(</span><span class="n">repo</span><span class="p">,</span><span class="w"> </span><span class="n">start</span><span class="p">,</span><span class="w"> </span><span class="n">time</span><span class="p" data-group-id="3991181523-18">)</span><span class="w">
    </span><span class="k" data-group-id="3991181523-14">end</span><span class="w">
  </span><span class="k" data-group-id="3991181523-12">end</span><span class="w">

  </span><span class="kd">defp</span><span class="w"> </span><span class="nf">canary</span><span class="p" data-group-id="3991181523-19">(</span><span class="n">repo</span><span class="p" data-group-id="3991181523-19">)</span><span class="w"> </span><span class="k" data-group-id="3991181523-20">do</span><span class="w">
    </span><span class="k">case</span><span class="w"> </span><span class="n">repo</span><span class="o">.</span><span class="n">query</span><span class="p" data-group-id="3991181523-21">(</span><span class="s">&quot;SELECT 1&quot;</span><span class="p" data-group-id="3991181523-21">)</span><span class="w"> </span><span class="k" data-group-id="3991181523-22">do</span><span class="w">
      </span><span class="p" data-group-id="3991181523-23">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="3991181523-24">%{</span><span class="ss">rows</span><span class="p">:</span><span class="w"> </span><span class="p" data-group-id="3991181523-25">[</span><span class="p" data-group-id="3991181523-26">[</span><span class="mi">1</span><span class="p" data-group-id="3991181523-26">]</span><span class="p" data-group-id="3991181523-25">]</span><span class="p" data-group-id="3991181523-24">}</span><span class="p" data-group-id="3991181523-23">}</span><span class="w"> </span><span class="o">-&gt;</span><span class="w"> </span><span class="ss">:ok</span><span class="w">
      </span><span class="bp">_</span><span class="w"> </span><span class="o">-&gt;</span><span class="w"> </span><span class="ss">:error</span><span class="w">
    </span><span class="k" data-group-id="3991181523-22">end</span><span class="w">
  </span><span class="k" data-group-id="3991181523-20">rescue</span><span class="w">
    </span><span class="bp">_</span><span class="w"> </span><span class="ow">in</span><span class="w"> </span><span class="nc">DBConnection.ConnectionError</span><span class="w"> </span><span class="o">-&gt;</span><span class="w"> </span><span class="ss">:error</span><span class="w">
  </span><span class="k" data-group-id="3991181523-20">end</span><span class="w">

  </span><span class="c1"># ...</span><span class="w">
</span><span class="k" data-group-id="3991181523-1">end</span></code></pre>
<p>
This resolved the flakey deployments.</p>
<h2>
<code class="inline">ExAWS</code> (2023-11-28)</h2>
<p>
AWS RDS supports <a href="https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/UsingWithRDS.IAMDBAuth.html">IAM database authentication</a>. This means that we don’t have to deal with password rotation and can instead use shortlived tokens as database passwords!</p>
<p>
To set up RDS IAM database authentication you need to <a href="https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/UsingWithRDS.IAMDBAuth.Enabling.html">enable RDS IAM authentication</a> first and ensure that your database user has <a href="https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/UsingWithRDS.IAMDBAuth.DBAccounts.html">RDS IAM authentication enabled</a>.</p>
<p>
In postgres this requires running a <code class="inline">GRANT rds_iam TO REPLACE_WITH_DB_USERNAME;</code> query.</p>
<p>
The token must be generated and used as the database password each time Ecto sets up a connection as the token will only be valid for 15 minutes.</p>
<p>
We’ll use <a href="https://github.com/ex-aws/ex_aws"><code class="inline">ExAWS</code></a> in the example below.</p>
<p>
First we’ll add the dependencies:</p>
<pre><code class="makeup elixir"><span class="kd">defp</span><span class="w"> </span><span class="nf">deps</span><span class="w"> </span><span class="k" data-group-id="3341077890-1">do</span><span class="w">
  </span><span class="c1"># ...</span><span class="w">

  </span><span class="c1"># To generate the token</span><span class="w">
  </span><span class="p" data-group-id="3341077890-2">{</span><span class="ss">:ex_aws</span><span class="p">,</span><span class="w"> </span><span class="s">&quot;~&gt; 2.4&quot;</span><span class="p" data-group-id="3341077890-2">}</span><span class="p">,</span><span class="w">

  </span><span class="c1"># CAStore for connecting to RDS with TLS</span><span class="w">
  </span><span class="p" data-group-id="3341077890-3">{</span><span class="ss">:aws_rds_castore</span><span class="p">,</span><span class="w"> </span><span class="s">&quot;~&gt; 1.1&quot;</span><span class="p" data-group-id="3341077890-3">}</span><span class="p">,</span><span class="w">
</span><span class="k" data-group-id="3341077890-1">end</span></code></pre>
<p>
Now we’ll update the repo config. Since the URL is generated on demand we don’t need to set the <code class="inline">:url</code> option. Instead, we will use the <code class="inline">:configure</code> callback. This is what I have in my <code class="inline">config/runtime.exs</code>:</p>
<pre><code class="makeup elixir"><span class="n">config</span><span class="w"> </span><span class="ss">:my_app</span><span class="p">,</span><span class="w"> </span><span class="nc">MyApp.Repo</span><span class="p">,</span><span class="w">
  </span><span class="ss">configure</span><span class="p">:</span><span class="w"> </span><span class="p" data-group-id="1456024892-1">{</span><span class="w">
    </span><span class="nc">MyApp.Repo</span><span class="p">,</span><span class="w"> 
    </span><span class="ss">:configure_with_auth_token</span><span class="p">,</span><span class="w">
    </span><span class="p" data-group-id="1456024892-2">[</span><span class="w">
      </span><span class="ss">host</span><span class="p">:</span><span class="w"> </span><span class="n">host</span><span class="p">,</span><span class="w">
      </span><span class="ss">username</span><span class="p">:</span><span class="w"> </span><span class="n">username</span><span class="p">,</span><span class="w">
      </span><span class="ss">dbname</span><span class="p">:</span><span class="w"> </span><span class="n">dbname</span><span class="p">,</span><span class="w">
      </span><span class="ss">port</span><span class="p">:</span><span class="w"> </span><span class="n">port</span><span class="w">
    </span><span class="p" data-group-id="1456024892-2">]</span><span class="w">
  </span><span class="p" data-group-id="1456024892-1">}</span></code></pre>
<p>
And you should also make sure that <code class="inline">ExAws</code> is started when running migrations in <code class="inline">config/config.exs</code>:</p>
<pre><code class="makeup elixir"><span class="n">config</span><span class="w"> </span><span class="ss">:my_app</span><span class="p">,</span><span class="w"> </span><span class="nc">MyApp.Repo</span><span class="p">,</span><span class="w">
  </span><span class="c1"># ExAWS is used in prod to generate the IAM DB password token</span><span class="w">
  </span><span class="ss">start_apps_before_migration</span><span class="p">:</span><span class="w"> </span><span class="p" data-group-id="1450913450-1">[</span><span class="ss">:ssl</span><span class="p">,</span><span class="w"> </span><span class="ss">:logger</span><span class="p">,</span><span class="w"> </span><span class="ss">:ex_aws</span><span class="p" data-group-id="1450913450-1">]</span></code></pre>
<p>
The last piece is to implement the <code class="inline">configure_with_auth_token/2</code> function:</p>
<pre><code class="makeup elixir"><span class="kd">defmodule</span><span class="w"> </span><span class="nc">MyApp.Repo</span><span class="w"> </span><span class="k" data-group-id="6511463520-1">do</span><span class="w">
  </span><span class="kn">use</span><span class="w"> </span><span class="nc">Ecto.Repo</span><span class="p">,</span><span class="w">
    </span><span class="ss">otp_app</span><span class="p">:</span><span class="w"> </span><span class="ss">:my_app</span><span class="p">,</span><span class="w">
    </span><span class="ss">adapter</span><span class="p">:</span><span class="w"> </span><span class="nc">Ecto.Adapters.Postgres</span><span class="w">

  </span><span class="c1"># Helper function to configure the connection with dynamically generated</span><span class="w">
  </span><span class="c1"># auth token for the IAM instance role</span><span class="w">
  </span><span class="kd">def</span><span class="w"> </span><span class="nf">configure_with_auth_token</span><span class="p" data-group-id="6511463520-2">(</span><span class="n">opts</span><span class="p">,</span><span class="w"> </span><span class="n">credentials</span><span class="p" data-group-id="6511463520-2">)</span><span class="w"> </span><span class="k" data-group-id="6511463520-3">do</span><span class="w">
    </span><span class="n">hostname</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">Keyword</span><span class="o">.</span><span class="n">fetch!</span><span class="p" data-group-id="6511463520-4">(</span><span class="n">credentials</span><span class="p">,</span><span class="w"> </span><span class="ss">:host</span><span class="p" data-group-id="6511463520-4">)</span><span class="w">
    </span><span class="n">username</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">Keyword</span><span class="o">.</span><span class="n">fetch!</span><span class="p" data-group-id="6511463520-5">(</span><span class="n">credentials</span><span class="p">,</span><span class="w"> </span><span class="ss">:username</span><span class="p" data-group-id="6511463520-5">)</span><span class="w">
    </span><span class="n">port</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">Keyword</span><span class="o">.</span><span class="n">fetch!</span><span class="p" data-group-id="6511463520-6">(</span><span class="n">credentials</span><span class="p">,</span><span class="w"> </span><span class="ss">:port</span><span class="p" data-group-id="6511463520-6">)</span><span class="w">
    </span><span class="n">dbname</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">Keyword</span><span class="o">.</span><span class="n">fetch!</span><span class="p" data-group-id="6511463520-7">(</span><span class="n">credentials</span><span class="p">,</span><span class="w"> </span><span class="ss">:dbname</span><span class="p" data-group-id="6511463520-7">)</span><span class="w">
    </span><span class="n">region</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">Keyword</span><span class="o">.</span><span class="n">fetch!</span><span class="p" data-group-id="6511463520-8">(</span><span class="n">credentials</span><span class="p">,</span><span class="w"> </span><span class="ss">:region</span><span class="p" data-group-id="6511463520-8">)</span><span class="w">

    </span><span class="n">aws_credentials</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">:aws_credentials</span><span class="o">.</span><span class="n">get_credentials</span><span class="p" data-group-id="6511463520-9">(</span><span class="p" data-group-id="6511463520-9">)</span><span class="w">
    </span><span class="n">auth_token</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">rds_auth_token</span><span class="p" data-group-id="6511463520-10">(</span><span class="n">aws_credentials</span><span class="p">,</span><span class="w"> </span><span class="n">hostname</span><span class="p">,</span><span class="w"> </span><span class="n">port</span><span class="p">,</span><span class="w"> </span><span class="n">username</span><span class="p">,</span><span class="w"> </span><span class="n">region</span><span class="p" data-group-id="6511463520-10">)</span><span class="w">

    </span><span class="nc">Keyword</span><span class="o">.</span><span class="n">merge</span><span class="p" data-group-id="6511463520-11">(</span><span class="n">opts</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="6511463520-12">[</span><span class="w">
      </span><span class="ss">hostname</span><span class="p">:</span><span class="w"> </span><span class="n">hostname</span><span class="p">,</span><span class="w">
      </span><span class="ss">port</span><span class="p">:</span><span class="w"> </span><span class="n">port</span><span class="p">,</span><span class="w">
      </span><span class="ss">username</span><span class="p">:</span><span class="w"> </span><span class="n">username</span><span class="p">,</span><span class="w">
      </span><span class="ss">password</span><span class="p">:</span><span class="w"> </span><span class="n">auth_token</span><span class="p">,</span><span class="w">
      </span><span class="ss">database</span><span class="p">:</span><span class="w"> </span><span class="n">dbname</span><span class="p">,</span><span class="w">
      </span><span class="ss">ssl</span><span class="p">:</span><span class="w"> </span><span class="no">true</span><span class="p">,</span><span class="w">
      </span><span class="ss">ssl_opts</span><span class="p">:</span><span class="w"> </span><span class="nc">AwsRdsCAStore</span><span class="o">.</span><span class="n">ssl_opts</span><span class="p" data-group-id="6511463520-13">(</span><span class="n">hostname</span><span class="p" data-group-id="6511463520-13">)</span><span class="w">
    </span><span class="p" data-group-id="6511463520-12">]</span><span class="p" data-group-id="6511463520-11">)</span><span class="w">
  </span><span class="k" data-group-id="6511463520-3">end</span><span class="w">

  </span><span class="kd">defp</span><span class="w"> </span><span class="nf">rds_auth_token</span><span class="p" data-group-id="6511463520-14">(</span><span class="n">aws_credentials</span><span class="p">,</span><span class="w"> </span><span class="n">hostname</span><span class="p">,</span><span class="w"> </span><span class="n">port</span><span class="p">,</span><span class="w"> </span><span class="n">username</span><span class="p">,</span><span class="w"> </span><span class="n">region</span><span class="p">,</span><span class="w"> </span><span class="n">opts</span><span class="w"> </span><span class="o">\\</span><span class="w"> </span><span class="p" data-group-id="6511463520-15">[</span><span class="ss">ttl</span><span class="p">:</span><span class="w"> </span><span class="mi">900</span><span class="p" data-group-id="6511463520-15">]</span><span class="p" data-group-id="6511463520-14">)</span><span class="w"> </span><span class="k" data-group-id="6511463520-16">do</span><span class="w">
    </span><span class="n">access_key</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">Map</span><span class="o">.</span><span class="n">fetch!</span><span class="p" data-group-id="6511463520-17">(</span><span class="n">aws_credentials</span><span class="p">,</span><span class="w"> </span><span class="ss">:access_key_id</span><span class="p" data-group-id="6511463520-17">)</span><span class="w">
    </span><span class="n">secret_key</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">Map</span><span class="o">.</span><span class="n">fetch!</span><span class="p" data-group-id="6511463520-18">(</span><span class="n">aws_credentials</span><span class="p">,</span><span class="w"> </span><span class="ss">:secret_access_key</span><span class="p" data-group-id="6511463520-18">)</span><span class="w">
    </span><span class="n">datetime</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">:erlang</span><span class="o">.</span><span class="n">universaltime</span><span class="p" data-group-id="6511463520-19">(</span><span class="p" data-group-id="6511463520-19">)</span><span class="w">
    </span><span class="n">url</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s">&quot;https://</span><span class="si" data-group-id="6511463520-20">#{</span><span class="n">hostname</span><span class="si" data-group-id="6511463520-20">}</span><span class="s">:</span><span class="si" data-group-id="6511463520-21">#{</span><span class="n">port</span><span class="si" data-group-id="6511463520-21">}</span><span class="s">/?Action=connect&amp;DBUser=</span><span class="si" data-group-id="6511463520-22">#{</span><span class="n">username</span><span class="si" data-group-id="6511463520-22">}</span><span class="s">&quot;</span><span class="w">

    </span><span class="n">signed_url</span><span class="w"> </span><span class="o">=</span><span class="w">
      </span><span class="nc">:aws_signature</span><span class="o">.</span><span class="n">sign_v4_query_params</span><span class="p" data-group-id="6511463520-23">(</span><span class="w">
        </span><span class="n">access_key</span><span class="p">,</span><span class="w">
        </span><span class="n">secret_key</span><span class="p">,</span><span class="w">
        </span><span class="n">region</span><span class="p">,</span><span class="w">
        </span><span class="s">&quot;rds-db&quot;</span><span class="p">,</span><span class="w">
        </span><span class="n">datetime</span><span class="p">,</span><span class="w">
        </span><span class="n">url</span><span class="p">,</span><span class="w">
        </span><span class="n">opts</span><span class="w">
      </span><span class="p" data-group-id="6511463520-23">)</span><span class="w">

    </span><span class="nc">String</span><span class="o">.</span><span class="n">trim_leading</span><span class="p" data-group-id="6511463520-24">(</span><span class="n">signed_url</span><span class="p">,</span><span class="w"> </span><span class="s">&quot;https://&quot;</span><span class="p" data-group-id="6511463520-24">)</span><span class="w">
  </span><span class="k" data-group-id="6511463520-16">end</span><span class="w">
</span><span class="k" data-group-id="6511463520-1">end</span></code></pre>
<p>
Now you got RDS IAM database authentication running!</p>

      ]]>
    </content>
  </entry>
  
  <entry>
    <title>When to use field :default in an Ecto schema</title>
    <link href="https://danschultzer.com/posts/when-to-use-field-default-in-ecto" />
    <id>https://danschultzer.com/posts/when-to-use-field-default-in-ecto</id>
    <updated>2023-08-06T00:00:00Z</updated>
    <summary>Understanding how the Ecto schema field option `:default` works, and when to use it.</summary>
    <content type="html">
      <![CDATA[
        <p>
What does the <code class="inline">:default</code> option actually do when you set it on a field in an Ecto schema? This question came up when dealing with this scenario:</p>
<pre><code class="makeup elixir"><span class="kd">defmodule</span><span class="w"> </span><span class="nc">MyApp.Repo.Migrations.CreatePosts</span><span class="w"> </span><span class="k" data-group-id="8780902069-1">do</span><span class="w">
  </span><span class="kn">use</span><span class="w"> </span><span class="nc">Ecto.Migration</span><span class="w">

  </span><span class="kd">def</span><span class="w"> </span><span class="nf">up</span><span class="w"> </span><span class="k" data-group-id="8780902069-2">do</span><span class="w">
    </span><span class="n">create</span><span class="w"> </span><span class="n">table</span><span class="p" data-group-id="8780902069-3">(</span><span class="ss">:posts</span><span class="p" data-group-id="8780902069-3">)</span><span class="w"> </span><span class="k" data-group-id="8780902069-4">do</span><span class="w">
      </span><span class="n">add</span><span class="w"> </span><span class="ss">:category</span><span class="p">,</span><span class="w"> </span><span class="ss">:string</span><span class="w">

      </span><span class="n">timestamps</span><span class="p" data-group-id="8780902069-5">(</span><span class="p" data-group-id="8780902069-5">)</span><span class="w">
    </span><span class="k" data-group-id="8780902069-4">end</span><span class="w">
  </span><span class="k" data-group-id="8780902069-2">end</span><span class="w">
</span><span class="k" data-group-id="8780902069-1">end</span></code></pre>
<pre><code class="makeup elixir"><span class="kd">defmodule</span><span class="w"> </span><span class="nc">MyApp.Posts.Post</span><span class="w"> </span><span class="k" data-group-id="8276819785-1">do</span><span class="w">
  </span><span class="kn">use</span><span class="w"> </span><span class="nc">Ecto.Schema</span><span class="w">
  </span><span class="kn">import</span><span class="w"> </span><span class="nc">Ecto.Changeset</span><span class="w">

  </span><span class="n">schema</span><span class="w"> </span><span class="s">&quot;posts&quot;</span><span class="w"> </span><span class="k" data-group-id="8276819785-2">do</span><span class="w">
    </span><span class="n">field</span><span class="w"> </span><span class="ss">:category</span><span class="p">,</span><span class="w"> </span><span class="ss">:string</span><span class="p">,</span><span class="w"> </span><span class="ss">default</span><span class="p">:</span><span class="w"> </span><span class="s">&quot;all&quot;</span><span class="w">

    </span><span class="n">timestamps</span><span class="p" data-group-id="8276819785-3">(</span><span class="p" data-group-id="8276819785-3">)</span><span class="w">
  </span><span class="k" data-group-id="8276819785-2">end</span><span class="w">

  </span><span class="kd">def</span><span class="w"> </span><span class="nf">changeset</span><span class="p" data-group-id="8276819785-4">(</span><span class="n">changeset</span><span class="p">,</span><span class="w"> </span><span class="n">attrs</span><span class="p" data-group-id="8276819785-4">)</span><span class="w"> </span><span class="k" data-group-id="8276819785-5">do</span><span class="w">
    </span><span class="n">changeset</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">cast</span><span class="p" data-group-id="8276819785-6">(</span><span class="n">attrs</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="8276819785-7">[</span><span class="ss">:category</span><span class="p" data-group-id="8276819785-7">]</span><span class="p" data-group-id="8276819785-6">)</span><span class="w">
  </span><span class="k" data-group-id="8276819785-5">end</span><span class="w">
</span><span class="k" data-group-id="8276819785-1">end</span></code></pre>
<p>
A <code class="inline">:default</code> option is set on the schema field, but not in the migration column. This irked me. What does that mean? What is the guarantee here?</p>
<p>
I needed to dig deeper to understand how <code class="inline">:default</code> is used. According to the Ecto docs for <a href="https://hexdocs.pm/ecto/3.10.3/Ecto.Schema.html#field/3"><code class="inline">Ecto.Schema.field/3</code></a>, <code class="inline">:default</code> sets the default value on the schema and the struct. Sounds like it’s just a <code class="inline">defstruct</code> call. Going into the bowels of Ecto I found out that was correct.</p>
<p>
The field <code class="inline">:default</code> is used in the <code class="inline">@ecto_struct_fields</code> compile-time attribute. <code class="inline">@ecto_struct_fields</code> is <a href="https://github.com/elixir-ecto/ecto/blob/f2a0def734dcee354acb223116550e9ae659fb0c/lib/ecto/schema.ex#L2242-L2250">a list of <code class="inline">{key, default_or_assoc}</code> tuples</a> that’s used in <a href="https://github.com/elixir-ecto/ecto/blob/f2a0def734dcee354acb223116550e9ae659fb0c/lib/ecto/schema.ex#L639">a <code class="inline">defstruct</code> call</a> like so:</p>
<pre><code class="makeup elixir"><span class="kd">defstruct</span><span class="w"> </span><span class="nc">Enum</span><span class="o">.</span><span class="n">reverse</span><span class="p" data-group-id="6190433833-1">(</span><span class="na">@ecto_struct_fields</span><span class="p" data-group-id="6190433833-1">)</span></code></pre>
<p>
This means that setting the <code class="inline">:default</code> option on a <code class="inline">field</code> is identical to this:</p>
<pre><code class="makeup elixir"><span class="kd">defmodule</span><span class="w"> </span><span class="nc">Post</span><span class="w"> </span><span class="k" data-group-id="1616960353-1">do</span><span class="w">
  </span><span class="kd">defstruct</span><span class="w"> </span><span class="ss">category</span><span class="p">:</span><span class="w"> </span><span class="s">&quot;all&quot;</span><span class="w">
</span><span class="k" data-group-id="1616960353-1">end</span></code></pre>
<p>
What’s the difference between this and setting the <code class="inline">:default</code> option for <a href="https://hexdocs.pm/ecto_sql/Ecto.Migration.html#add/3"><code class="inline">Ecto.Migration.add/3</code></a>?</p>
<p>
When you set the <code class="inline">:default</code> option in the migration it will be used in the SQL as <code class="inline">DEFAULT</code> for the column definition like so:</p>
<pre><code class="sql">CREATE TABLE posts
(
  &#39;category&#39; varchar(255) SET DEFAULT &#39;all&#39;
);</code></pre>
<p>
Now let’s think of the consequences. What happens if we batch insert with <a href="https://hexdocs.pm/ecto/3.10.3/Ecto.Repo.html#c:insert_all/3"><code class="inline">Ecto.Repo.insert_all/3</code></a>?</p>
<pre><code class="makeup elixir"><span class="gp unselectable">iex(1)&gt; </span><span class="n">now</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p" data-group-id="7975798429-1">%{</span><span class="nc">NaiveDateTime</span><span class="o">.</span><span class="n">utc_now</span><span class="p" data-group-id="7975798429-2">(</span><span class="p" data-group-id="7975798429-2">)</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="ss">microsecond</span><span class="p">:</span><span class="w"> </span><span class="p" data-group-id="7975798429-3">{</span><span class="mi">0</span><span class="p">,</span><span class="w"> </span><span class="mi">0</span><span class="p" data-group-id="7975798429-3">}</span><span class="p" data-group-id="7975798429-1">}</span><span class="w">
</span><span class="ld">~N[2023-08-06 14:25:42]</span><span class="w">

</span><span class="gp unselectable">iex(2)&gt; </span><span class="nc">MyApp.Repo</span><span class="o">.</span><span class="n">insert_all</span><span class="p" data-group-id="7975798429-4">(</span><span class="nc">MyApp.Posts.Post</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="7975798429-5">[</span><span class="p" data-group-id="7975798429-6">%{</span><span class="ss">inserted_at</span><span class="p">:</span><span class="w"> </span><span class="n">now</span><span class="p">,</span><span class="w"> </span><span class="ss">updated_at</span><span class="p">:</span><span class="w"> </span><span class="n">now</span><span class="p" data-group-id="7975798429-6">}</span><span class="p" data-group-id="7975798429-5">]</span><span class="p" data-group-id="7975798429-4">)</span><span class="w">
</span><span class="p" data-group-id="7975798429-7">{</span><span class="mi">1</span><span class="p">,</span><span class="w"> </span><span class="no">nil</span><span class="p" data-group-id="7975798429-7">}</span></code></pre>
<pre><code class="makeup elixir"><span class="gp unselectable">iex(3)&gt; </span><span class="nc">MyApp.Repo</span><span class="o">.</span><span class="n">all</span><span class="p" data-group-id="1214925125-1">(</span><span class="nc">MyApp.Posts.Post</span><span class="p" data-group-id="1214925125-1">)</span><span class="w">
</span><span class="p" data-group-id="1214925125-2">[</span><span class="w">
  </span><span class="p" data-group-id="1214925125-3">%</span><span class="nc" data-group-id="1214925125-3">MyApp.Posts.Post</span><span class="p" data-group-id="1214925125-3">{</span><span class="w">
    </span><span class="ss">__meta__</span><span class="p">:</span><span class="w"> </span><span class="p" data-group-id="1214925125-4">#</span><span class="nc" data-group-id="1214925125-4">Ecto.Schema.Metadata</span><span class="p" data-group-id="1214925125-4">&lt;</span><span class="ss">:loaded</span><span class="p">,</span><span class="w"> </span><span class="s">&quot;posts&quot;</span><span class="p" data-group-id="1214925125-4">&gt;</span><span class="p">,</span><span class="w">
    </span><span class="ss">id</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span><span class="w">
    </span><span class="ss">category</span><span class="p">:</span><span class="w"> </span><span class="no">nil</span><span class="p">,</span><span class="w">
    </span><span class="ss">inserted_at</span><span class="p">:</span><span class="w"> </span><span class="ld">~N[2023-08-06 14:25:42]</span><span class="p">,</span><span class="w">
    </span><span class="ss">updated_at</span><span class="p">:</span><span class="w"> </span><span class="ld">~N[2023-08-06 14:25:42]</span><span class="w">
  </span><span class="p" data-group-id="1214925125-3">}</span><span class="w">
</span><span class="p" data-group-id="1214925125-2">]</span></code></pre>
<p>
Oh-uh, the category is <code class="inline">nil</code>! This makes sense as <code class="inline">:default</code> is only part of the struct, and the struct fields will be overridden with the values from the database when loaded. Thus <code class="inline">:default</code> is only used when we initialize a new struct.</p>
<p>
One of the wonderful things about Ecto is how it makes developers let the database deal with what should be database concerns (for example <a href="https://hexdocs.pm/ecto/3.10.3/Ecto.Changeset.html#unique_constraint/3"><code class="inline">Ecto.Changeset.unique_constraint/3</code></a>). I think the same should be said for the <code class="inline">:default</code> option. Just let the database deal with it!</p>
<p>
Let’s ask ourselves:</p>
<ul>
  <li>
    <p>
Should it be possible to have a <code class="inline">nil</code> value on a field? If not, set <code class="inline">null: false</code> in the migration.    </p>
  </li>
  <li>
    <p>
Is there an expectation of a default value in the database? If yes, set <code class="inline">default: &quot;value&quot;</code> in the migration.    </p>
  </li>
  <li>
    <p>
When should you use the <code class="inline">:default</code> option on the schema field?    </p>
    <p>
If you always require a value (ensured with a <code class="inline">NOT NULL</code> constraint in the database) and need some default value before running through the changeset. But even in that case, I would ask if there is a more explicit way to handle that, for example in the context function:    </p>
    <pre><code class="makeup elixir"><span class="kd">defmodule</span><span class="w"> </span><span class="nc">MyApp.Posts</span><span class="w"> </span><span class="k" data-group-id="3348391725-1">do</span><span class="w">
  </span><span class="kn">alias</span><span class="w"> </span><span class="nc">MyApp</span><span class="o">.</span><span class="p" data-group-id="3348391725-2">{</span><span class="nc">Posts.Post</span><span class="p">,</span><span class="w"> </span><span class="nc">Repo</span><span class="p" data-group-id="3348391725-2">}</span><span class="w">

  </span><span class="na">@default_category</span><span class="w"> </span><span class="s">&quot;all&quot;</span><span class="w">

  </span><span class="kd">def</span><span class="w"> </span><span class="nf">create_post</span><span class="p" data-group-id="3348391725-3">(</span><span class="n">attrs</span><span class="p" data-group-id="3348391725-3">)</span><span class="w"> </span><span class="k" data-group-id="3348391725-4">do</span><span class="w">
    </span><span class="p" data-group-id="3348391725-5">%</span><span class="nc" data-group-id="3348391725-5">Post</span><span class="p" data-group-id="3348391725-5">{</span><span class="ss">category</span><span class="p">:</span><span class="w"> </span><span class="na">@default_category</span><span class="p" data-group-id="3348391725-5">}</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="nc">Post</span><span class="o">.</span><span class="n">changeset</span><span class="p" data-group-id="3348391725-6">(</span><span class="n">attrs</span><span class="p" data-group-id="3348391725-6">)</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="nc">Repo</span><span class="o">.</span><span class="n">insert</span><span class="p" data-group-id="3348391725-7">(</span><span class="p" data-group-id="3348391725-7">)</span><span class="w">
  </span><span class="k" data-group-id="3348391725-4">end</span><span class="w">
</span><span class="k" data-group-id="3348391725-1">end</span></code></pre>
  </li>
</ul>
<h3>
Embedded schema</h3>
<p>
<a href="http://www.brian-underwood.codes/">Brian Underwood</a> points out that one of the main reasons for <code class="inline">:default</code> existing <a href="https://genserver.social/notice/AYaBd7iipi0CPoeL3o#.">could be database-less schemas</a>:</p>
<blockquote>
  <p>
<a href="https://mastodon.online/@danschultzer">@danschultzer</a> Just read your post about Ecto defaults. I’ll bet that one of the main reasons why `default` exists is for when you’re using Ecto without a database since people often use Ecto for validating / transforming data from various sources without necessarily storing it.  </p>
</blockquote>
<p>
A database-less schema is normally kept in memory using <a href="https://hexdocs.pm/ecto/Ecto.Schema.html#embedded_schema/1"><code class="inline">Ecto.Schema.embedded_schema/1</code></a>:</p>
<pre><code class="makeup elixir"><span class="kd">defmodule</span><span class="w"> </span><span class="nc">MyApp.Posts.Post</span><span class="w"> </span><span class="k" data-group-id="8056196074-1">do</span><span class="w">
  </span><span class="kn">use</span><span class="w"> </span><span class="nc">Ecto.Schema</span><span class="w">

  </span><span class="n">embedded_schema</span><span class="w"> </span><span class="k" data-group-id="8056196074-2">do</span><span class="w">
    </span><span class="n">field</span><span class="w"> </span><span class="ss">:category</span><span class="p">,</span><span class="w"> </span><span class="ss">:string</span><span class="p">,</span><span class="w"> </span><span class="ss">default</span><span class="p">:</span><span class="w"> </span><span class="s">&quot;all&quot;</span><span class="w">
  </span><span class="k" data-group-id="8056196074-2">end</span><span class="w">
</span><span class="k" data-group-id="8056196074-1">end</span></code></pre>
<p>
Again, what does <code class="inline">:default</code> mean here? Should the source be trusted to not populate a <code class="inline">nil</code> value on a field that has the <code class="inline">:default</code> option?</p>
<pre><code class="makeup elixir"><span class="gp unselectable">iex(1)&gt; </span><span class="n">data</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p" data-group-id="5190480425-1">%{</span><span class="s">&quot;category&quot;</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="no">nil</span><span class="p" data-group-id="5190480425-1">}</span><span class="w">
</span><span class="p" data-group-id="5190480425-2">%{</span><span class="s">&quot;category&quot;</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="no">nil</span><span class="p" data-group-id="5190480425-2">}</span><span class="w">

</span><span class="gp unselectable">iex(2)&gt; </span><span class="nc">Ecto</span><span class="o">.</span><span class="n">embedded_load</span><span class="p" data-group-id="5190480425-3">(</span><span class="nc">MyApp.Posts.Post</span><span class="p">,</span><span class="w"> </span><span class="n">data</span><span class="p">,</span><span class="w"> </span><span class="ss">:json</span><span class="p" data-group-id="5190480425-3">)</span><span class="w">
</span><span class="p" data-group-id="5190480425-4">%</span><span class="nc" data-group-id="5190480425-4">MyApp.Posts.Post</span><span class="p" data-group-id="5190480425-4">{</span><span class="ss">id</span><span class="p">:</span><span class="w"> </span><span class="no">nil</span><span class="p">,</span><span class="w"> </span><span class="ss">category</span><span class="p">:</span><span class="w"> </span><span class="no">nil</span><span class="p" data-group-id="5190480425-4">}</span></code></pre>
<p>
We have no guarantees as the <code class="inline">:default</code> option only helps us with initialization:</p>
<pre><code class="makeup elixir"><span class="n">iex</span><span class="p" data-group-id="0705055128-1">(</span><span class="mi">3</span><span class="p" data-group-id="0705055128-1">)</span><span class="w"> </span><span class="nc">Ecto</span><span class="o">.</span><span class="n">embedded_dump</span><span class="p" data-group-id="0705055128-2">(</span><span class="p" data-group-id="0705055128-3">%</span><span class="nc" data-group-id="0705055128-3">MyApp.Posts.Post</span><span class="p" data-group-id="0705055128-3">{</span><span class="p" data-group-id="0705055128-3">}</span><span class="p">,</span><span class="w"> </span><span class="ss">:json</span><span class="p" data-group-id="0705055128-2">)</span><span class="w">  
</span><span class="p" data-group-id="0705055128-4">%{</span><span class="ss">id</span><span class="p">:</span><span class="w"> </span><span class="no">nil</span><span class="p">,</span><span class="w"> </span><span class="ss">category</span><span class="p">:</span><span class="w"> </span><span class="s">&quot;all&quot;</span><span class="p" data-group-id="0705055128-4">}</span><span class="w">

</span><span class="n">iex</span><span class="p" data-group-id="0705055128-5">(</span><span class="mi">4</span><span class="p" data-group-id="0705055128-5">)</span><span class="w"> </span><span class="nc">Ecto</span><span class="o">.</span><span class="n">embedded_dump</span><span class="p" data-group-id="0705055128-6">(</span><span class="p" data-group-id="0705055128-7">%</span><span class="nc" data-group-id="0705055128-7">MyApp.Posts.Post</span><span class="p" data-group-id="0705055128-7">{</span><span class="ss">category</span><span class="p">:</span><span class="w"> </span><span class="no">nil</span><span class="p" data-group-id="0705055128-7">}</span><span class="p">,</span><span class="w"> </span><span class="ss">:json</span><span class="p" data-group-id="0705055128-6">)</span><span class="w">
</span><span class="p" data-group-id="0705055128-8">%{</span><span class="ss">id</span><span class="p">:</span><span class="w"> </span><span class="no">nil</span><span class="p">,</span><span class="w"> </span><span class="ss">category</span><span class="p">:</span><span class="w"> </span><span class="no">nil</span><span class="p" data-group-id="0705055128-8">}</span></code></pre>
<p>
It’s safer to transform the source data to ensure that fields don’t have a <code class="inline">nil</code> value if we depend on that:</p>
<pre><code class="makeup elixir"><span class="n">iex</span><span class="p" data-group-id="1002213593-1">(</span><span class="mi">4</span><span class="p" data-group-id="1002213593-1">)</span><span class="w"> </span><span class="n">data</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">Map</span><span class="o">.</span><span class="n">update</span><span class="p" data-group-id="1002213593-2">(</span><span class="n">data</span><span class="p">,</span><span class="w"> </span><span class="s">&quot;category&quot;</span><span class="p">,</span><span class="w"> </span><span class="no">nil</span><span class="p">,</span><span class="w"> </span><span class="o">&amp;</span><span class="w"> </span><span class="ni">&amp;1</span><span class="w"> </span><span class="o">||</span><span class="w"> </span><span class="s">&quot;all&quot;</span><span class="p" data-group-id="1002213593-2">)</span><span class="w">
</span><span class="p" data-group-id="1002213593-3">%{</span><span class="s">&quot;category&quot;</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="s">&quot;all&quot;</span><span class="p" data-group-id="1002213593-3">}</span><span class="w">

</span><span class="n">iex</span><span class="p" data-group-id="1002213593-4">(</span><span class="mi">5</span><span class="p" data-group-id="1002213593-4">)</span><span class="w"> </span><span class="nc">Ecto</span><span class="o">.</span><span class="n">embedded_load</span><span class="p" data-group-id="1002213593-5">(</span><span class="nc">MyApp.Posts.Post</span><span class="p">,</span><span class="w"> </span><span class="n">data</span><span class="p">,</span><span class="w"> </span><span class="ss">:json</span><span class="p" data-group-id="1002213593-5">)</span><span class="w">
</span><span class="p" data-group-id="1002213593-6">%</span><span class="nc" data-group-id="1002213593-6">MyApp.Posts.Post</span><span class="p" data-group-id="1002213593-6">{</span><span class="ss">id</span><span class="p">:</span><span class="w"> </span><span class="no">nil</span><span class="p">,</span><span class="w"> </span><span class="ss">category</span><span class="p">:</span><span class="w"> </span><span class="s">&quot;all&quot;</span><span class="p" data-group-id="1002213593-6">}</span></code></pre>
<p>
This is in the same vein as how we would enforce no <code class="inline">nil</code> value with a database constraint, though we have no control over storage here.</p>

      ]]>
    </content>
  </entry>
  
  <entry>
    <title>Prefixed base62 UUIDv7 Object IDs with Ecto</title>
    <link href="https://danschultzer.com/posts/prefixed-base62-uuidv7-object-ids-with-ecto" />
    <id>https://danschultzer.com/posts/prefixed-base62-uuidv7-object-ids-with-ecto</id>
    <updated>2023-06-11T00:00:00Z</updated>
    <summary>How I set up Ecto to use prefixed base62 UUIDv7 Object IDs.</summary>
    <content type="html">
      <![CDATA[
        <p>
Ecto supports integer IDs and UUIDv4 out-of-the-box, but I needed both UUIDv7 and human-readable Object IDs. This was a breeze to implement in Ecto.</p>
<h2>
ID, UUIDv7, and Object ID</h2>
<p>
Incremental integer IDs (e.g. <code class="inline">21</code>) are easy to get started with. They take up little space and have a lexicographical order (aka index locality) which makes sorting easy. However, there are many downsides. They are vulnerable to enumeration attacks, can leak sensitive information, scaling and replication become a pain with key conflicts, and you can’t generate the ID offline (in your application logic) since we must know the previous ID.</p>
<p>
UUID (e.g. <code class="inline">3bff9b42-f5e4-425e-aca2-39b490dfc878</code>) solves all these issues by generating a unique string of alphanumeric characters. These can be generated offline and inserted as-is into the database. The risk of collision is extremely low. There are five different <a href="https://datatracker.ietf.org/doc/html/rfc4122">standard versions</a>. UUIDv4 is generally the preferred UUID version because of its low collision risk and only carries random data. It is what <code class="inline">Ecto.UUID</code> generates. The downside is that UUIDv4 doesn’t have index locality which makes sorting on the ID practically impossible.</p>
<p>
UUIDv7 (e.g. <code class="inline">0188aadc-f449-7818-8862-5eff12733f64</code>) is a proposed standard that solves the index locality issue of UUIDv4 by making the first part of the UUIDv7 a Unix epoch timestamp. It’s the favored UUID version in place of UUIDv4 in the <a href="https://www.ietf.org/archive/id/draft-peabody-dispatch-new-uuid-format-01.html">New UUID Formats IETF draft</a>. We’ll use UUIDv7 in our database.</p>
<p>
Externally, in our API and web interface, we would like human-readable Object IDs. The UUID format doesn’t read that well in URIs; <code class="inline">/accounts/0188aadc-f449-7818-8862-5eff12733f64</code>. We can use base62 encoding to shorten it and get rid of the hyphens; <code class="inline">/accounts/02tRrww6GFm4urcMhyQpAS</code>. Object IDs should carry context so we will add prefixes to the IDs: <code class="inline">/accounts/acct_02tRrww6GFm4urcMhyQpAS</code>.</p>
<p>
Why base62? Base62 is just base64URL without the <code class="inline">-</code> and <code class="inline">_</code> characters. We are already using underscore to separate prefixes and IDs and I think hyphens make the ID read worse. With base62 we get a short alphanumeric ID without special characters.</p>
<p>
The Object ID becomes very useful with polymorphic fields and for debugging. We will know what type of resource we’re dealing with by just looking at the ID. There’s much more to this which has been covered in length in the blog post <a href="https://dev.to/stripe/designing-apis-for-humans-object-ids-3o5a">Designing APIs for humans: Object IDs by Paul Asjes</a>.</p>
<p>
Let us continue with the Ecto implementation.</p>
<h2>
Prefixed base62 UUIDv7 in Ecto</h2>
<p>
First, we’ll install the <code class="inline">Uniq</code> library to generate UUIDv7:</p>
<pre><code class="makeup elixir"><span class="c1"># mix.exs</span><span class="w">
</span><span class="kd">defmodule</span><span class="w"> </span><span class="nc">MyApp.MixProject</span><span class="w"> </span><span class="k" data-group-id="5396599082-1">do</span><span class="w">
  </span><span class="kn">use</span><span class="w"> </span><span class="nc">Mix.Project</span><span class="w">

  </span><span class="c1"># ...</span><span class="w">

  </span><span class="kd">def</span><span class="w"> </span><span class="nf">deps</span><span class="w"> </span><span class="k" data-group-id="5396599082-2">do</span><span class="w">
    </span><span class="p" data-group-id="5396599082-3">[</span><span class="w">
      </span><span class="c1"># ...</span><span class="w">
      </span><span class="p" data-group-id="5396599082-4">{</span><span class="ss">:uniq</span><span class="p">,</span><span class="w"> </span><span class="s">&quot;~&gt; 0.5.0&quot;</span><span class="p" data-group-id="5396599082-4">}</span><span class="w">
    </span><span class="p" data-group-id="5396599082-3">]</span><span class="w">
  </span><span class="k" data-group-id="5396599082-2">end</span><span class="w">

  </span><span class="c1">#...</span><span class="w">
</span><span class="k" data-group-id="5396599082-1">end</span></code></pre>
<p>
Now we’ll set up a custom <code class="inline">Ecto.ParameterizedType</code> module that base62 encodes and prefixes the UUIDs.</p>
<p>
We’ll first set up the <code class="inline">init/1</code> callback to require the prefix. <code class="inline">init/1</code> also gets called on associations, where no extra options like the prefix can be added, so we must differentiate between when it’s the primary key or foreign key.</p>
<p>
<code class="inline">Uniq</code> is initialized with raw UUIDv7 format.</p>
<pre><code class="makeup elixir"><span class="kd">defmodule</span><span class="w"> </span><span class="nc">MyApp.PrefixedUUID</span><span class="w"> </span><span class="k" data-group-id="0623309680-1">do</span><span class="w">
  </span><span class="na">@doc</span><span class="w"> </span><span class="s">&quot;&quot;&quot;
  Generates prefixed base62 encoded UUIDv7.

  ## Examples

      @primary_key {:id, MyApp.PrefixedUUID, prefix: &quot;acct&quot;, autogenerate: true}
      @foreign_key_type MyApp.PrefixedUUID
  &quot;&quot;&quot;</span><span class="w">
  </span><span class="kn">use</span><span class="w"> </span><span class="nc">Ecto.ParameterizedType</span><span class="w">

  </span><span class="na">@impl</span><span class="w"> </span><span class="no">true</span><span class="w">
  </span><span class="kd">def</span><span class="w"> </span><span class="nf">init</span><span class="p" data-group-id="0623309680-2">(</span><span class="n">opts</span><span class="p" data-group-id="0623309680-2">)</span><span class="w"> </span><span class="k" data-group-id="0623309680-3">do</span><span class="w">
    </span><span class="n">schema</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">Keyword</span><span class="o">.</span><span class="n">fetch!</span><span class="p" data-group-id="0623309680-4">(</span><span class="n">opts</span><span class="p">,</span><span class="w"> </span><span class="ss">:schema</span><span class="p" data-group-id="0623309680-4">)</span><span class="w">
    </span><span class="n">field</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">Keyword</span><span class="o">.</span><span class="n">fetch!</span><span class="p" data-group-id="0623309680-5">(</span><span class="n">opts</span><span class="p">,</span><span class="w"> </span><span class="ss">:field</span><span class="p" data-group-id="0623309680-5">)</span><span class="w">
    </span><span class="n">uniq</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">Uniq.UUID</span><span class="o">.</span><span class="n">init</span><span class="p" data-group-id="0623309680-6">(</span><span class="ss">schema</span><span class="p">:</span><span class="w"> </span><span class="n">schema</span><span class="p">,</span><span class="w"> </span><span class="ss">field</span><span class="p">:</span><span class="w"> </span><span class="n">field</span><span class="p">,</span><span class="w"> </span><span class="ss">version</span><span class="p">:</span><span class="w"> </span><span class="mi">7</span><span class="p">,</span><span class="w"> </span><span class="ss">default</span><span class="p">:</span><span class="w"> </span><span class="ss">:raw</span><span class="p">,</span><span class="w"> </span><span class="ss">dump</span><span class="p">:</span><span class="w"> </span><span class="ss">:raw</span><span class="p" data-group-id="0623309680-6">)</span><span class="w">

    </span><span class="k">case</span><span class="w"> </span><span class="n">opts</span><span class="p" data-group-id="0623309680-7">[</span><span class="ss">:primary_key</span><span class="p" data-group-id="0623309680-7">]</span><span class="w"> </span><span class="k" data-group-id="0623309680-8">do</span><span class="w">
      </span><span class="no">true</span><span class="w"> </span><span class="o">-&gt;</span><span class="w">
        </span><span class="n">prefix</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">Keyword</span><span class="o">.</span><span class="n">get</span><span class="p" data-group-id="0623309680-9">(</span><span class="n">opts</span><span class="p">,</span><span class="w"> </span><span class="ss">:prefix</span><span class="p" data-group-id="0623309680-9">)</span><span class="w"> </span><span class="o">||</span><span class="w"> </span><span class="k">raise</span><span class="w"> </span><span class="s">&quot;`:prefix` option is required&quot;</span><span class="w">

        </span><span class="p" data-group-id="0623309680-10">%{</span><span class="w">
          </span><span class="ss">primary_key</span><span class="p">:</span><span class="w"> </span><span class="no">true</span><span class="p">,</span><span class="w">
          </span><span class="ss">schema</span><span class="p">:</span><span class="w"> </span><span class="n">schema</span><span class="p">,</span><span class="w">
          </span><span class="ss">prefix</span><span class="p">:</span><span class="w"> </span><span class="n">prefix</span><span class="p">,</span><span class="w">
          </span><span class="ss">uniq</span><span class="p">:</span><span class="w"> </span><span class="n">uniq</span><span class="w">
        </span><span class="p" data-group-id="0623309680-10">}</span><span class="w">

      </span><span class="c">_any</span><span class="w"> </span><span class="o">-&gt;</span><span class="w">
        </span><span class="p" data-group-id="0623309680-11">%{</span><span class="w">
          </span><span class="ss">schema</span><span class="p">:</span><span class="w"> </span><span class="n">schema</span><span class="p">,</span><span class="w">
          </span><span class="ss">field</span><span class="p">:</span><span class="w"> </span><span class="n">field</span><span class="p">,</span><span class="w">
          </span><span class="ss">uniq</span><span class="p">:</span><span class="w"> </span><span class="n">uniq</span><span class="w">
        </span><span class="p" data-group-id="0623309680-11">}</span><span class="w">
    </span><span class="k" data-group-id="0623309680-8">end</span><span class="w">
  </span><span class="k" data-group-id="0623309680-3">end</span><span class="w">

  </span><span class="na">@impl</span><span class="w"> </span><span class="no">true</span><span class="w">
  </span><span class="kd">def</span><span class="w"> </span><span class="nf">type</span><span class="p" data-group-id="0623309680-12">(</span><span class="c">_params</span><span class="p" data-group-id="0623309680-12">)</span><span class="p">,</span><span class="w"> </span><span class="ss">do</span><span class="p">:</span><span class="w"> </span><span class="ss">:uuid</span><span class="w">

  </span><span class="c1"># ...</span><span class="w">
</span><span class="k" data-group-id="0623309680-1">end</span></code></pre>
<p>
Next, we’ll implement <code class="inline">cast/2</code> and <code class="inline">dump/3</code> which decodes the slug into the raw UUID. <code class="inline">cast/2</code> gets called on external input (not from the database), and the output will be used for the <code class="inline">dump/3</code> callback. <code class="inline">dump/3</code> is called when we got data to be inserted into the database.</p>
<p>
To decode the slug we first split it into the prefix and encoded components, then decode the encoded UUID. We ensure the prefix matches the module prefix. If we’re dealing with a belongs_to association, we must fetch the prefix option from the related schema module.</p>
<p>
Note that nil is handled as the belongs_to field could be optional.</p>
<pre><code class="makeup elixir"><span class="w">  </span><span class="na">@impl</span><span class="w"> </span><span class="no">true</span><span class="w">
  </span><span class="kd">def</span><span class="w"> </span><span class="nf">cast</span><span class="p" data-group-id="5188188165-1">(</span><span class="no">nil</span><span class="p">,</span><span class="w"> </span><span class="c">_params</span><span class="p" data-group-id="5188188165-1">)</span><span class="p">,</span><span class="w"> </span><span class="ss">do</span><span class="p">:</span><span class="w"> </span><span class="p" data-group-id="5188188165-2">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="no">nil</span><span class="p" data-group-id="5188188165-2">}</span><span class="w">

  </span><span class="kd">def</span><span class="w"> </span><span class="nf">cast</span><span class="p" data-group-id="5188188165-3">(</span><span class="n">data</span><span class="p">,</span><span class="w"> </span><span class="n">params</span><span class="p" data-group-id="5188188165-3">)</span><span class="w"> </span><span class="k" data-group-id="5188188165-4">do</span><span class="w">
    </span><span class="k">with</span><span class="w"> </span><span class="p" data-group-id="5188188165-5">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="n">prefix</span><span class="p">,</span><span class="w"> </span><span class="c">_uuid</span><span class="p" data-group-id="5188188165-5">}</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="n">slug_to_uuid</span><span class="p" data-group-id="5188188165-6">(</span><span class="n">data</span><span class="p">,</span><span class="w"> </span><span class="n">params</span><span class="p" data-group-id="5188188165-6">)</span><span class="p">,</span><span class="w">
         </span><span class="p" data-group-id="5188188165-7">{</span><span class="n">prefix</span><span class="p">,</span><span class="w"> </span><span class="n">prefix</span><span class="p" data-group-id="5188188165-7">}</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="p" data-group-id="5188188165-8">{</span><span class="n">prefix</span><span class="p">,</span><span class="w"> </span><span class="n">prefix</span><span class="p" data-group-id="5188188165-9">(</span><span class="n">params</span><span class="p" data-group-id="5188188165-9">)</span><span class="p" data-group-id="5188188165-8">}</span><span class="w"> </span><span class="k" data-group-id="5188188165-10">do</span><span class="w">
      </span><span class="p" data-group-id="5188188165-11">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="n">data</span><span class="p" data-group-id="5188188165-11">}</span><span class="w">
    </span><span class="k" data-group-id="5188188165-10">else</span><span class="w">
      </span><span class="bp">_</span><span class="w"> </span><span class="o">-&gt;</span><span class="w"> </span><span class="ss">:error</span><span class="w">
    </span><span class="k" data-group-id="5188188165-10">end</span><span class="w">
  </span><span class="k" data-group-id="5188188165-4">end</span><span class="w">

  </span><span class="kd">defp</span><span class="w"> </span><span class="nf">slug_to_uuid</span><span class="p" data-group-id="5188188165-12">(</span><span class="n">string</span><span class="p">,</span><span class="w"> </span><span class="c">_params</span><span class="p" data-group-id="5188188165-12">)</span><span class="w"> </span><span class="k" data-group-id="5188188165-13">do</span><span class="w">
    </span><span class="k">with</span><span class="w"> </span><span class="p" data-group-id="5188188165-14">[</span><span class="n">prefix</span><span class="p">,</span><span class="w"> </span><span class="n">slug</span><span class="p" data-group-id="5188188165-14">]</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="nc">String</span><span class="o">.</span><span class="n">split</span><span class="p" data-group-id="5188188165-15">(</span><span class="n">string</span><span class="p">,</span><span class="w"> </span><span class="s">&quot;_&quot;</span><span class="p" data-group-id="5188188165-15">)</span><span class="p">,</span><span class="w">
         </span><span class="p" data-group-id="5188188165-16">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="n">uuid</span><span class="p" data-group-id="5188188165-16">}</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="n">decode_base62_uuid</span><span class="p" data-group-id="5188188165-17">(</span><span class="n">slug</span><span class="p" data-group-id="5188188165-17">)</span><span class="w"> </span><span class="k" data-group-id="5188188165-18">do</span><span class="w">
      </span><span class="p" data-group-id="5188188165-19">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="n">prefix</span><span class="p">,</span><span class="w"> </span><span class="n">uuid</span><span class="p" data-group-id="5188188165-19">}</span><span class="w">
    </span><span class="k" data-group-id="5188188165-18">else</span><span class="w">
      </span><span class="bp">_</span><span class="w"> </span><span class="o">-&gt;</span><span class="w"> </span><span class="ss">:error</span><span class="w">
    </span><span class="k" data-group-id="5188188165-18">end</span><span class="w">
  </span><span class="k" data-group-id="5188188165-13">end</span><span class="w">

  </span><span class="kd">defp</span><span class="w"> </span><span class="nf">prefix</span><span class="p" data-group-id="5188188165-20">(</span><span class="p" data-group-id="5188188165-21">%{</span><span class="ss">primary_key</span><span class="p">:</span><span class="w"> </span><span class="no">true</span><span class="p">,</span><span class="w"> </span><span class="ss">prefix</span><span class="p">:</span><span class="w"> </span><span class="n">prefix</span><span class="p" data-group-id="5188188165-21">}</span><span class="p" data-group-id="5188188165-20">)</span><span class="p">,</span><span class="w"> </span><span class="ss">do</span><span class="p">:</span><span class="w"> </span><span class="n">prefix</span><span class="w">

  </span><span class="c1"># If we deal with a belongs_to assocation we need to fetch the prefix from</span><span class="w">
  </span><span class="c1"># the associations schema module</span><span class="w">
  </span><span class="kd">defp</span><span class="w"> </span><span class="nf">prefix</span><span class="p" data-group-id="5188188165-22">(</span><span class="p" data-group-id="5188188165-23">%{</span><span class="ss">schema</span><span class="p">:</span><span class="w"> </span><span class="n">schema</span><span class="p">,</span><span class="w"> </span><span class="ss">field</span><span class="p">:</span><span class="w"> </span><span class="n">field</span><span class="p" data-group-id="5188188165-23">}</span><span class="p" data-group-id="5188188165-22">)</span><span class="w"> </span><span class="k" data-group-id="5188188165-24">do</span><span class="w">
    </span><span class="p" data-group-id="5188188165-25">%{</span><span class="ss">related</span><span class="p">:</span><span class="w"> </span><span class="n">schema</span><span class="p">,</span><span class="w"> </span><span class="ss">related_key</span><span class="p">:</span><span class="w"> </span><span class="n">field</span><span class="p" data-group-id="5188188165-25">}</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">schema</span><span class="o">.</span><span class="c">__schema__</span><span class="p" data-group-id="5188188165-26">(</span><span class="ss">:association</span><span class="p">,</span><span class="w"> </span><span class="n">field</span><span class="p" data-group-id="5188188165-26">)</span><span class="w">
    </span><span class="p" data-group-id="5188188165-27">{</span><span class="ss">:parameterized</span><span class="p">,</span><span class="w"> </span><span class="bp">__MODULE__</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="5188188165-28">%{</span><span class="ss">prefix</span><span class="p">:</span><span class="w"> </span><span class="n">prefix</span><span class="p" data-group-id="5188188165-28">}</span><span class="p" data-group-id="5188188165-27">}</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">schema</span><span class="o">.</span><span class="c">__schema__</span><span class="p" data-group-id="5188188165-29">(</span><span class="ss">:type</span><span class="p">,</span><span class="w"> </span><span class="n">field</span><span class="p" data-group-id="5188188165-29">)</span><span class="w">

    </span><span class="n">prefix</span><span class="w">
  </span><span class="k" data-group-id="5188188165-24">end</span><span class="w">

  </span><span class="na">@impl</span><span class="w"> </span><span class="no">true</span><span class="w">
  </span><span class="kd">def</span><span class="w"> </span><span class="nf">dump</span><span class="p" data-group-id="5188188165-30">(</span><span class="no">nil</span><span class="p">,</span><span class="w"> </span><span class="bp">_</span><span class="p">,</span><span class="w"> </span><span class="bp">_</span><span class="p" data-group-id="5188188165-30">)</span><span class="p">,</span><span class="w"> </span><span class="ss">do</span><span class="p">:</span><span class="w"> </span><span class="p" data-group-id="5188188165-31">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="no">nil</span><span class="p" data-group-id="5188188165-31">}</span><span class="w">

  </span><span class="kd">def</span><span class="w"> </span><span class="nf">dump</span><span class="p" data-group-id="5188188165-32">(</span><span class="n">slug</span><span class="p">,</span><span class="w"> </span><span class="n">dumper</span><span class="p">,</span><span class="w"> </span><span class="n">params</span><span class="p" data-group-id="5188188165-32">)</span><span class="w"> </span><span class="k" data-group-id="5188188165-33">do</span><span class="w">
    </span><span class="k">case</span><span class="w"> </span><span class="n">slug_to_uuid</span><span class="p" data-group-id="5188188165-34">(</span><span class="n">slug</span><span class="p">,</span><span class="w"> </span><span class="n">params</span><span class="p" data-group-id="5188188165-34">)</span><span class="w"> </span><span class="k" data-group-id="5188188165-35">do</span><span class="w">
      </span><span class="p" data-group-id="5188188165-36">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="c">_prefix</span><span class="p">,</span><span class="w"> </span><span class="n">uuid</span><span class="p" data-group-id="5188188165-36">}</span><span class="w"> </span><span class="o">-&gt;</span><span class="w"> </span><span class="nc">Uniq.UUID</span><span class="o">.</span><span class="n">dump</span><span class="p" data-group-id="5188188165-37">(</span><span class="n">uuid</span><span class="p">,</span><span class="w"> </span><span class="n">dumper</span><span class="p">,</span><span class="w"> </span><span class="n">params</span><span class="o">.</span><span class="n">uniq</span><span class="p" data-group-id="5188188165-37">)</span><span class="w">
      </span><span class="ss">:error</span><span class="w"> </span><span class="o">-&gt;</span><span class="w"> </span><span class="ss">:error</span><span class="w">
    </span><span class="k" data-group-id="5188188165-35">end</span><span class="w">
  </span><span class="k" data-group-id="5188188165-33">end</span></code></pre>
<p>
Now we’ll implement <code class="inline">load/3</code> and <code class="inline">autogenerate/1</code> that encodes the UUID. <code class="inline">load/3</code> gets called when we get the UUID from the database. Note that nil is handled here again as the belongs_to field could be optional and thus nil in the database.</p>
<p>
<code class="inline">autogenerate/1</code> calls <code class="inline">Uniq</code> to generate a new UUIDv7. To encode the UUID we fetch the prefix and prepend it to the base62 encoded UUID string.</p>
<pre><code class="makeup elixir"><span class="w">  </span><span class="na">@impl</span><span class="w"> </span><span class="no">true</span><span class="w">
  </span><span class="kd">def</span><span class="w"> </span><span class="nf">load</span><span class="p" data-group-id="6279655379-1">(</span><span class="n">data</span><span class="p">,</span><span class="w"> </span><span class="n">loader</span><span class="p">,</span><span class="w"> </span><span class="n">params</span><span class="p" data-group-id="6279655379-1">)</span><span class="w"> </span><span class="k" data-group-id="6279655379-2">do</span><span class="w">
    </span><span class="k">case</span><span class="w"> </span><span class="nc">Uniq.UUID</span><span class="o">.</span><span class="n">load</span><span class="p" data-group-id="6279655379-3">(</span><span class="n">data</span><span class="p">,</span><span class="w"> </span><span class="n">loader</span><span class="p">,</span><span class="w"> </span><span class="n">params</span><span class="o">.</span><span class="n">uniq</span><span class="p" data-group-id="6279655379-3">)</span><span class="w"> </span><span class="k" data-group-id="6279655379-4">do</span><span class="w">
      </span><span class="p" data-group-id="6279655379-5">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="no">nil</span><span class="p" data-group-id="6279655379-5">}</span><span class="w"> </span><span class="o">-&gt;</span><span class="w"> </span><span class="p" data-group-id="6279655379-6">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="no">nil</span><span class="p" data-group-id="6279655379-6">}</span><span class="w">
      </span><span class="p" data-group-id="6279655379-7">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="n">uuid</span><span class="p" data-group-id="6279655379-7">}</span><span class="w"> </span><span class="o">-&gt;</span><span class="w"> </span><span class="p" data-group-id="6279655379-8">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="n">uuid_to_slug</span><span class="p" data-group-id="6279655379-9">(</span><span class="n">uuid</span><span class="p">,</span><span class="w"> </span><span class="n">params</span><span class="p" data-group-id="6279655379-9">)</span><span class="p" data-group-id="6279655379-8">}</span><span class="w">
      </span><span class="ss">:error</span><span class="w"> </span><span class="o">-&gt;</span><span class="w"> </span><span class="ss">:error</span><span class="w">
    </span><span class="k" data-group-id="6279655379-4">end</span><span class="w">
  </span><span class="k" data-group-id="6279655379-2">end</span><span class="w">

  </span><span class="kd">defp</span><span class="w"> </span><span class="nf">uuid_to_slug</span><span class="p" data-group-id="6279655379-10">(</span><span class="n">uuid</span><span class="p">,</span><span class="w"> </span><span class="n">params</span><span class="p" data-group-id="6279655379-10">)</span><span class="w"> </span><span class="k" data-group-id="6279655379-11">do</span><span class="w">
    </span><span class="s">&quot;</span><span class="si" data-group-id="6279655379-12">#{</span><span class="n">prefix</span><span class="p" data-group-id="6279655379-13">(</span><span class="n">params</span><span class="p" data-group-id="6279655379-13">)</span><span class="si" data-group-id="6279655379-12">}</span><span class="s">_</span><span class="si" data-group-id="6279655379-14">#{</span><span class="n">encode_base62_uuid</span><span class="p" data-group-id="6279655379-15">(</span><span class="n">uuid</span><span class="p" data-group-id="6279655379-15">)</span><span class="si" data-group-id="6279655379-14">}</span><span class="s">&quot;</span><span class="w">
  </span><span class="k" data-group-id="6279655379-11">end</span><span class="w">

  </span><span class="na">@impl</span><span class="w"> </span><span class="no">true</span><span class="w">
  </span><span class="kd">def</span><span class="w"> </span><span class="nf">autogenerate</span><span class="p" data-group-id="6279655379-16">(</span><span class="n">params</span><span class="p" data-group-id="6279655379-16">)</span><span class="w"> </span><span class="k" data-group-id="6279655379-17">do</span><span class="w">
    </span><span class="n">uuid_to_slug</span><span class="p" data-group-id="6279655379-18">(</span><span class="nc">Uniq.UUID</span><span class="o">.</span><span class="n">autogenerate</span><span class="p" data-group-id="6279655379-19">(</span><span class="n">params</span><span class="o">.</span><span class="n">uniq</span><span class="p" data-group-id="6279655379-19">)</span><span class="p">,</span><span class="w"> </span><span class="n">params</span><span class="p" data-group-id="6279655379-18">)</span><span class="w">
  </span><span class="k" data-group-id="6279655379-17">end</span></code></pre>
<p>
Now we got prefixed base62 encoded UUIDv7 working! All that’s left is to implement the base62 encoder/decoder (<code class="inline">encode_base62_uuid/1</code> and <code class="inline">decode_base62_uuid/1</code>). </p>
<p>
Let’s put it all together and then implement it into our app. Here, we got the complete module and test module:</p>
<pre><code class="makeup elixir"><span class="kd">defmodule</span><span class="w"> </span><span class="nc">MyApp.PrefixedUUID</span><span class="w"> </span><span class="k" data-group-id="6257658676-1">do</span><span class="w">
  </span><span class="na">@doc</span><span class="w"> </span><span class="s">&quot;&quot;&quot;
  Generates prefixed base62 encoded UUIDv7.

  ## Examples

      @primary_key {:id, MyApp.PrefixedUUID, prefix: &quot;acct&quot;, autogenerate: true}
      @foreign_key_type MyApp.PrefixedUUID
  &quot;&quot;&quot;</span><span class="w">
  </span><span class="kn">use</span><span class="w"> </span><span class="nc">Ecto.ParameterizedType</span><span class="w">

  </span><span class="na">@impl</span><span class="w"> </span><span class="no">true</span><span class="w">
  </span><span class="kd">def</span><span class="w"> </span><span class="nf">init</span><span class="p" data-group-id="6257658676-2">(</span><span class="n">opts</span><span class="p" data-group-id="6257658676-2">)</span><span class="w"> </span><span class="k" data-group-id="6257658676-3">do</span><span class="w">
    </span><span class="n">schema</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">Keyword</span><span class="o">.</span><span class="n">fetch!</span><span class="p" data-group-id="6257658676-4">(</span><span class="n">opts</span><span class="p">,</span><span class="w"> </span><span class="ss">:schema</span><span class="p" data-group-id="6257658676-4">)</span><span class="w">
    </span><span class="n">field</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">Keyword</span><span class="o">.</span><span class="n">fetch!</span><span class="p" data-group-id="6257658676-5">(</span><span class="n">opts</span><span class="p">,</span><span class="w"> </span><span class="ss">:field</span><span class="p" data-group-id="6257658676-5">)</span><span class="w">
    </span><span class="n">uniq</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">Uniq.UUID</span><span class="o">.</span><span class="n">init</span><span class="p" data-group-id="6257658676-6">(</span><span class="ss">schema</span><span class="p">:</span><span class="w"> </span><span class="n">schema</span><span class="p">,</span><span class="w"> </span><span class="ss">field</span><span class="p">:</span><span class="w"> </span><span class="n">field</span><span class="p">,</span><span class="w"> </span><span class="ss">version</span><span class="p">:</span><span class="w"> </span><span class="mi">7</span><span class="p">,</span><span class="w"> </span><span class="ss">default</span><span class="p">:</span><span class="w"> </span><span class="ss">:raw</span><span class="p">,</span><span class="w"> </span><span class="ss">dump</span><span class="p">:</span><span class="w"> </span><span class="ss">:raw</span><span class="p" data-group-id="6257658676-6">)</span><span class="w">

    </span><span class="k">case</span><span class="w"> </span><span class="n">opts</span><span class="p" data-group-id="6257658676-7">[</span><span class="ss">:primary_key</span><span class="p" data-group-id="6257658676-7">]</span><span class="w"> </span><span class="k" data-group-id="6257658676-8">do</span><span class="w">
      </span><span class="no">true</span><span class="w"> </span><span class="o">-&gt;</span><span class="w">
        </span><span class="n">prefix</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">Keyword</span><span class="o">.</span><span class="n">get</span><span class="p" data-group-id="6257658676-9">(</span><span class="n">opts</span><span class="p">,</span><span class="w"> </span><span class="ss">:prefix</span><span class="p" data-group-id="6257658676-9">)</span><span class="w"> </span><span class="o">||</span><span class="w"> </span><span class="k">raise</span><span class="w"> </span><span class="s">&quot;`:prefix` option is required&quot;</span><span class="w">

        </span><span class="p" data-group-id="6257658676-10">%{</span><span class="w">
          </span><span class="ss">primary_key</span><span class="p">:</span><span class="w"> </span><span class="no">true</span><span class="p">,</span><span class="w">
          </span><span class="ss">schema</span><span class="p">:</span><span class="w"> </span><span class="n">schema</span><span class="p">,</span><span class="w">
          </span><span class="ss">prefix</span><span class="p">:</span><span class="w"> </span><span class="n">prefix</span><span class="p">,</span><span class="w">
          </span><span class="ss">uniq</span><span class="p">:</span><span class="w"> </span><span class="n">uniq</span><span class="w">
        </span><span class="p" data-group-id="6257658676-10">}</span><span class="w">

      </span><span class="c">_any</span><span class="w"> </span><span class="o">-&gt;</span><span class="w">
        </span><span class="p" data-group-id="6257658676-11">%{</span><span class="w">
          </span><span class="ss">schema</span><span class="p">:</span><span class="w"> </span><span class="n">schema</span><span class="p">,</span><span class="w">
          </span><span class="ss">field</span><span class="p">:</span><span class="w"> </span><span class="n">field</span><span class="p">,</span><span class="w">
          </span><span class="ss">uniq</span><span class="p">:</span><span class="w"> </span><span class="n">uniq</span><span class="w">
        </span><span class="p" data-group-id="6257658676-11">}</span><span class="w">
    </span><span class="k" data-group-id="6257658676-8">end</span><span class="w">
  </span><span class="k" data-group-id="6257658676-3">end</span><span class="w">

  </span><span class="na">@impl</span><span class="w"> </span><span class="no">true</span><span class="w">
  </span><span class="kd">def</span><span class="w"> </span><span class="nf">type</span><span class="p" data-group-id="6257658676-12">(</span><span class="c">_params</span><span class="p" data-group-id="6257658676-12">)</span><span class="p">,</span><span class="w"> </span><span class="ss">do</span><span class="p">:</span><span class="w"> </span><span class="ss">:uuid</span><span class="w">

  </span><span class="na">@impl</span><span class="w"> </span><span class="no">true</span><span class="w">
  </span><span class="kd">def</span><span class="w"> </span><span class="nf">cast</span><span class="p" data-group-id="6257658676-13">(</span><span class="no">nil</span><span class="p">,</span><span class="w"> </span><span class="c">_params</span><span class="p" data-group-id="6257658676-13">)</span><span class="p">,</span><span class="w"> </span><span class="ss">do</span><span class="p">:</span><span class="w"> </span><span class="p" data-group-id="6257658676-14">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="no">nil</span><span class="p" data-group-id="6257658676-14">}</span><span class="w">

  </span><span class="kd">def</span><span class="w"> </span><span class="nf">cast</span><span class="p" data-group-id="6257658676-15">(</span><span class="n">data</span><span class="p">,</span><span class="w"> </span><span class="n">params</span><span class="p" data-group-id="6257658676-15">)</span><span class="w"> </span><span class="k" data-group-id="6257658676-16">do</span><span class="w">
    </span><span class="k">with</span><span class="w"> </span><span class="p" data-group-id="6257658676-17">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="n">prefix</span><span class="p">,</span><span class="w"> </span><span class="c">_uuid</span><span class="p" data-group-id="6257658676-17">}</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="n">slug_to_uuid</span><span class="p" data-group-id="6257658676-18">(</span><span class="n">data</span><span class="p">,</span><span class="w"> </span><span class="n">params</span><span class="p" data-group-id="6257658676-18">)</span><span class="p">,</span><span class="w">
         </span><span class="p" data-group-id="6257658676-19">{</span><span class="n">prefix</span><span class="p">,</span><span class="w"> </span><span class="n">prefix</span><span class="p" data-group-id="6257658676-19">}</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="p" data-group-id="6257658676-20">{</span><span class="n">prefix</span><span class="p">,</span><span class="w"> </span><span class="n">prefix</span><span class="p" data-group-id="6257658676-21">(</span><span class="n">params</span><span class="p" data-group-id="6257658676-21">)</span><span class="p" data-group-id="6257658676-20">}</span><span class="w"> </span><span class="k" data-group-id="6257658676-22">do</span><span class="w">
      </span><span class="p" data-group-id="6257658676-23">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="n">data</span><span class="p" data-group-id="6257658676-23">}</span><span class="w">
    </span><span class="k" data-group-id="6257658676-22">else</span><span class="w">
      </span><span class="bp">_</span><span class="w"> </span><span class="o">-&gt;</span><span class="w"> </span><span class="ss">:error</span><span class="w">
    </span><span class="k" data-group-id="6257658676-22">end</span><span class="w">
  </span><span class="k" data-group-id="6257658676-16">end</span><span class="w">

  </span><span class="kd">defp</span><span class="w"> </span><span class="nf">slug_to_uuid</span><span class="p" data-group-id="6257658676-24">(</span><span class="n">string</span><span class="p">,</span><span class="w"> </span><span class="c">_params</span><span class="p" data-group-id="6257658676-24">)</span><span class="w"> </span><span class="k" data-group-id="6257658676-25">do</span><span class="w">
    </span><span class="k">with</span><span class="w"> </span><span class="p" data-group-id="6257658676-26">[</span><span class="n">prefix</span><span class="p">,</span><span class="w"> </span><span class="n">slug</span><span class="p" data-group-id="6257658676-26">]</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="nc">String</span><span class="o">.</span><span class="n">split</span><span class="p" data-group-id="6257658676-27">(</span><span class="n">string</span><span class="p">,</span><span class="w"> </span><span class="s">&quot;_&quot;</span><span class="p" data-group-id="6257658676-27">)</span><span class="p">,</span><span class="w">
         </span><span class="p" data-group-id="6257658676-28">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="n">uuid</span><span class="p" data-group-id="6257658676-28">}</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="n">decode_base62_uuid</span><span class="p" data-group-id="6257658676-29">(</span><span class="n">slug</span><span class="p" data-group-id="6257658676-29">)</span><span class="w"> </span><span class="k" data-group-id="6257658676-30">do</span><span class="w">
      </span><span class="p" data-group-id="6257658676-31">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="n">prefix</span><span class="p">,</span><span class="w"> </span><span class="n">uuid</span><span class="p" data-group-id="6257658676-31">}</span><span class="w">
    </span><span class="k" data-group-id="6257658676-30">else</span><span class="w">
      </span><span class="bp">_</span><span class="w"> </span><span class="o">-&gt;</span><span class="w"> </span><span class="ss">:error</span><span class="w">
    </span><span class="k" data-group-id="6257658676-30">end</span><span class="w">
  </span><span class="k" data-group-id="6257658676-25">end</span><span class="w">

  </span><span class="kd">defp</span><span class="w"> </span><span class="nf">prefix</span><span class="p" data-group-id="6257658676-32">(</span><span class="p" data-group-id="6257658676-33">%{</span><span class="ss">primary_key</span><span class="p">:</span><span class="w"> </span><span class="no">true</span><span class="p">,</span><span class="w"> </span><span class="ss">prefix</span><span class="p">:</span><span class="w"> </span><span class="n">prefix</span><span class="p" data-group-id="6257658676-33">}</span><span class="p" data-group-id="6257658676-32">)</span><span class="p">,</span><span class="w"> </span><span class="ss">do</span><span class="p">:</span><span class="w"> </span><span class="n">prefix</span><span class="w">

  </span><span class="c1"># If we deal with a belongs_to assocation we need to fetch the prefix from</span><span class="w">
  </span><span class="c1"># the associations schema module</span><span class="w">
  </span><span class="kd">defp</span><span class="w"> </span><span class="nf">prefix</span><span class="p" data-group-id="6257658676-34">(</span><span class="p" data-group-id="6257658676-35">%{</span><span class="ss">schema</span><span class="p">:</span><span class="w"> </span><span class="n">schema</span><span class="p">,</span><span class="w"> </span><span class="ss">field</span><span class="p">:</span><span class="w"> </span><span class="n">field</span><span class="p" data-group-id="6257658676-35">}</span><span class="p" data-group-id="6257658676-34">)</span><span class="w"> </span><span class="k" data-group-id="6257658676-36">do</span><span class="w">
    </span><span class="p" data-group-id="6257658676-37">%{</span><span class="ss">related</span><span class="p">:</span><span class="w"> </span><span class="n">schema</span><span class="p">,</span><span class="w"> </span><span class="ss">related_key</span><span class="p">:</span><span class="w"> </span><span class="n">field</span><span class="p" data-group-id="6257658676-37">}</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">schema</span><span class="o">.</span><span class="c">__schema__</span><span class="p" data-group-id="6257658676-38">(</span><span class="ss">:association</span><span class="p">,</span><span class="w"> </span><span class="n">field</span><span class="p" data-group-id="6257658676-38">)</span><span class="w">
    </span><span class="p" data-group-id="6257658676-39">{</span><span class="ss">:parameterized</span><span class="p">,</span><span class="w"> </span><span class="bp">__MODULE__</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="6257658676-40">%{</span><span class="ss">prefix</span><span class="p">:</span><span class="w"> </span><span class="n">prefix</span><span class="p" data-group-id="6257658676-40">}</span><span class="p" data-group-id="6257658676-39">}</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">schema</span><span class="o">.</span><span class="c">__schema__</span><span class="p" data-group-id="6257658676-41">(</span><span class="ss">:type</span><span class="p">,</span><span class="w"> </span><span class="n">field</span><span class="p" data-group-id="6257658676-41">)</span><span class="w">

    </span><span class="n">prefix</span><span class="w">
  </span><span class="k" data-group-id="6257658676-36">end</span><span class="w">

  </span><span class="na">@impl</span><span class="w"> </span><span class="no">true</span><span class="w">
  </span><span class="kd">def</span><span class="w"> </span><span class="nf">load</span><span class="p" data-group-id="6257658676-42">(</span><span class="n">data</span><span class="p">,</span><span class="w"> </span><span class="n">loader</span><span class="p">,</span><span class="w"> </span><span class="n">params</span><span class="p" data-group-id="6257658676-42">)</span><span class="w"> </span><span class="k" data-group-id="6257658676-43">do</span><span class="w">
    </span><span class="k">case</span><span class="w"> </span><span class="nc">Uniq.UUID</span><span class="o">.</span><span class="n">load</span><span class="p" data-group-id="6257658676-44">(</span><span class="n">data</span><span class="p">,</span><span class="w"> </span><span class="n">loader</span><span class="p">,</span><span class="w"> </span><span class="n">params</span><span class="o">.</span><span class="n">uniq</span><span class="p" data-group-id="6257658676-44">)</span><span class="w"> </span><span class="k" data-group-id="6257658676-45">do</span><span class="w">
      </span><span class="p" data-group-id="6257658676-46">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="no">nil</span><span class="p" data-group-id="6257658676-46">}</span><span class="w"> </span><span class="o">-&gt;</span><span class="w"> </span><span class="p" data-group-id="6257658676-47">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="no">nil</span><span class="p" data-group-id="6257658676-47">}</span><span class="w">
      </span><span class="p" data-group-id="6257658676-48">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="n">uuid</span><span class="p" data-group-id="6257658676-48">}</span><span class="w"> </span><span class="o">-&gt;</span><span class="w"> </span><span class="p" data-group-id="6257658676-49">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="n">uuid_to_slug</span><span class="p" data-group-id="6257658676-50">(</span><span class="n">uuid</span><span class="p">,</span><span class="w"> </span><span class="n">params</span><span class="p" data-group-id="6257658676-50">)</span><span class="p" data-group-id="6257658676-49">}</span><span class="w">
      </span><span class="ss">:error</span><span class="w"> </span><span class="o">-&gt;</span><span class="w"> </span><span class="ss">:error</span><span class="w">
    </span><span class="k" data-group-id="6257658676-45">end</span><span class="w">
  </span><span class="k" data-group-id="6257658676-43">end</span><span class="w">

  </span><span class="kd">defp</span><span class="w"> </span><span class="nf">uuid_to_slug</span><span class="p" data-group-id="6257658676-51">(</span><span class="n">uuid</span><span class="p">,</span><span class="w"> </span><span class="n">params</span><span class="p" data-group-id="6257658676-51">)</span><span class="w"> </span><span class="k" data-group-id="6257658676-52">do</span><span class="w">
    </span><span class="s">&quot;</span><span class="si" data-group-id="6257658676-53">#{</span><span class="n">prefix</span><span class="p" data-group-id="6257658676-54">(</span><span class="n">params</span><span class="p" data-group-id="6257658676-54">)</span><span class="si" data-group-id="6257658676-53">}</span><span class="s">_</span><span class="si" data-group-id="6257658676-55">#{</span><span class="n">encode_base62_uuid</span><span class="p" data-group-id="6257658676-56">(</span><span class="n">uuid</span><span class="p" data-group-id="6257658676-56">)</span><span class="si" data-group-id="6257658676-55">}</span><span class="s">&quot;</span><span class="w">
  </span><span class="k" data-group-id="6257658676-52">end</span><span class="w">

  </span><span class="na">@impl</span><span class="w"> </span><span class="no">true</span><span class="w">
  </span><span class="kd">def</span><span class="w"> </span><span class="nf">dump</span><span class="p" data-group-id="6257658676-57">(</span><span class="no">nil</span><span class="p">,</span><span class="w"> </span><span class="bp">_</span><span class="p">,</span><span class="w"> </span><span class="bp">_</span><span class="p" data-group-id="6257658676-57">)</span><span class="p">,</span><span class="w"> </span><span class="ss">do</span><span class="p">:</span><span class="w"> </span><span class="p" data-group-id="6257658676-58">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="no">nil</span><span class="p" data-group-id="6257658676-58">}</span><span class="w">

  </span><span class="kd">def</span><span class="w"> </span><span class="nf">dump</span><span class="p" data-group-id="6257658676-59">(</span><span class="n">slug</span><span class="p">,</span><span class="w"> </span><span class="n">dumper</span><span class="p">,</span><span class="w"> </span><span class="n">params</span><span class="p" data-group-id="6257658676-59">)</span><span class="w"> </span><span class="k" data-group-id="6257658676-60">do</span><span class="w">
    </span><span class="k">case</span><span class="w"> </span><span class="n">slug_to_uuid</span><span class="p" data-group-id="6257658676-61">(</span><span class="n">slug</span><span class="p">,</span><span class="w"> </span><span class="n">params</span><span class="p" data-group-id="6257658676-61">)</span><span class="w"> </span><span class="k" data-group-id="6257658676-62">do</span><span class="w">
      </span><span class="p" data-group-id="6257658676-63">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="c">_prefix</span><span class="p">,</span><span class="w"> </span><span class="n">uuid</span><span class="p" data-group-id="6257658676-63">}</span><span class="w"> </span><span class="o">-&gt;</span><span class="w"> </span><span class="nc">Uniq.UUID</span><span class="o">.</span><span class="n">dump</span><span class="p" data-group-id="6257658676-64">(</span><span class="n">uuid</span><span class="p">,</span><span class="w"> </span><span class="n">dumper</span><span class="p">,</span><span class="w"> </span><span class="n">params</span><span class="o">.</span><span class="n">uniq</span><span class="p" data-group-id="6257658676-64">)</span><span class="w">
      </span><span class="ss">:error</span><span class="w"> </span><span class="o">-&gt;</span><span class="w"> </span><span class="ss">:error</span><span class="w">
    </span><span class="k" data-group-id="6257658676-62">end</span><span class="w">
  </span><span class="k" data-group-id="6257658676-60">end</span><span class="w">

  </span><span class="na">@impl</span><span class="w"> </span><span class="no">true</span><span class="w">
  </span><span class="kd">def</span><span class="w"> </span><span class="nf">autogenerate</span><span class="p" data-group-id="6257658676-65">(</span><span class="n">params</span><span class="p" data-group-id="6257658676-65">)</span><span class="w"> </span><span class="k" data-group-id="6257658676-66">do</span><span class="w">
    </span><span class="n">uuid_to_slug</span><span class="p" data-group-id="6257658676-67">(</span><span class="nc">Uniq.UUID</span><span class="o">.</span><span class="n">autogenerate</span><span class="p" data-group-id="6257658676-68">(</span><span class="n">params</span><span class="o">.</span><span class="n">uniq</span><span class="p" data-group-id="6257658676-68">)</span><span class="p">,</span><span class="w"> </span><span class="n">params</span><span class="p" data-group-id="6257658676-67">)</span><span class="w">
  </span><span class="k" data-group-id="6257658676-66">end</span><span class="w">

  </span><span class="na">@impl</span><span class="w"> </span><span class="no">true</span><span class="w">
  </span><span class="kd">def</span><span class="w"> </span><span class="nf">embed_as</span><span class="p" data-group-id="6257658676-69">(</span><span class="n">format</span><span class="p">,</span><span class="w"> </span><span class="n">params</span><span class="p" data-group-id="6257658676-69">)</span><span class="p">,</span><span class="w"> </span><span class="ss">do</span><span class="p">:</span><span class="w"> </span><span class="nc">Uniq.UUID</span><span class="o">.</span><span class="n">embed_as</span><span class="p" data-group-id="6257658676-70">(</span><span class="n">format</span><span class="p">,</span><span class="w"> </span><span class="n">params</span><span class="o">.</span><span class="n">uniq</span><span class="p" data-group-id="6257658676-70">)</span><span class="w">

  </span><span class="na">@impl</span><span class="w"> </span><span class="no">true</span><span class="w">
  </span><span class="kd">def</span><span class="w"> </span><span class="nf">equal?</span><span class="p" data-group-id="6257658676-71">(</span><span class="n">a</span><span class="p">,</span><span class="w"> </span><span class="n">b</span><span class="p">,</span><span class="w"> </span><span class="n">params</span><span class="p" data-group-id="6257658676-71">)</span><span class="p">,</span><span class="w"> </span><span class="ss">do</span><span class="p">:</span><span class="w"> </span><span class="nc">Uniq.UUID</span><span class="o">.</span><span class="n">equal?</span><span class="p" data-group-id="6257658676-72">(</span><span class="n">a</span><span class="p">,</span><span class="w"> </span><span class="n">b</span><span class="p">,</span><span class="w"> </span><span class="n">params</span><span class="o">.</span><span class="n">uniq</span><span class="p" data-group-id="6257658676-72">)</span><span class="w">

  </span><span class="c1"># UUID Base62 encoder/decoder</span><span class="w">

  </span><span class="na">@base62_uuid_length</span><span class="w"> </span><span class="mi">22</span><span class="w">
  </span><span class="na">@uuid_length</span><span class="w"> </span><span class="mi">32</span><span class="w">

  </span><span class="kd">defp</span><span class="w"> </span><span class="nf">encode_base62_uuid</span><span class="p" data-group-id="6257658676-73">(</span><span class="n">uuid</span><span class="p" data-group-id="6257658676-73">)</span><span class="w"> </span><span class="k" data-group-id="6257658676-74">do</span><span class="w">
    </span><span class="n">uuid</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="nc">String</span><span class="o">.</span><span class="n">replace</span><span class="p" data-group-id="6257658676-75">(</span><span class="s">&quot;-&quot;</span><span class="p">,</span><span class="w"> </span><span class="s">&quot;&quot;</span><span class="p" data-group-id="6257658676-75">)</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="nc">String</span><span class="o">.</span><span class="n">to_integer</span><span class="p" data-group-id="6257658676-76">(</span><span class="mi">16</span><span class="p" data-group-id="6257658676-76">)</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">base62_encode</span><span class="p" data-group-id="6257658676-77">(</span><span class="p" data-group-id="6257658676-77">)</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="nc">String</span><span class="o">.</span><span class="n">pad_leading</span><span class="p" data-group-id="6257658676-78">(</span><span class="na">@base62_uuid_length</span><span class="p">,</span><span class="w"> </span><span class="s">&quot;0&quot;</span><span class="p" data-group-id="6257658676-78">)</span><span class="w">
  </span><span class="k" data-group-id="6257658676-74">end</span><span class="w">

  </span><span class="kd">defp</span><span class="w"> </span><span class="nf">decode_base62_uuid</span><span class="p" data-group-id="6257658676-79">(</span><span class="n">string</span><span class="p" data-group-id="6257658676-79">)</span><span class="w"> </span><span class="k" data-group-id="6257658676-80">do</span><span class="w">
    </span><span class="k">with</span><span class="w"> </span><span class="p" data-group-id="6257658676-81">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="n">number</span><span class="p" data-group-id="6257658676-81">}</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="n">base62_decode</span><span class="p" data-group-id="6257658676-82">(</span><span class="n">string</span><span class="p" data-group-id="6257658676-82">)</span><span class="w"> </span><span class="k" data-group-id="6257658676-83">do</span><span class="w">
      </span><span class="n">number_to_uuid</span><span class="p" data-group-id="6257658676-84">(</span><span class="n">number</span><span class="p" data-group-id="6257658676-84">)</span><span class="w">
    </span><span class="k" data-group-id="6257658676-83">end</span><span class="w">
  </span><span class="k" data-group-id="6257658676-80">end</span><span class="w">

  </span><span class="kd">defp</span><span class="w"> </span><span class="nf">number_to_uuid</span><span class="p" data-group-id="6257658676-85">(</span><span class="n">number</span><span class="p" data-group-id="6257658676-85">)</span><span class="w"> </span><span class="k" data-group-id="6257658676-86">do</span><span class="w">
    </span><span class="n">number</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="nc">Integer</span><span class="o">.</span><span class="n">to_string</span><span class="p" data-group-id="6257658676-87">(</span><span class="mi">16</span><span class="p" data-group-id="6257658676-87">)</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="nc">String</span><span class="o">.</span><span class="n">downcase</span><span class="p" data-group-id="6257658676-88">(</span><span class="p" data-group-id="6257658676-88">)</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="nc">String</span><span class="o">.</span><span class="n">pad_leading</span><span class="p" data-group-id="6257658676-89">(</span><span class="na">@uuid_length</span><span class="p">,</span><span class="w"> </span><span class="s">&quot;0&quot;</span><span class="p" data-group-id="6257658676-89">)</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="k">case</span><span class="w"> </span><span class="k" data-group-id="6257658676-90">do</span><span class="w">
      </span><span class="p" data-group-id="6257658676-91">&lt;&lt;</span><span class="n">g1</span><span class="o">::</span><span class="n">binary</span><span class="o">-</span><span class="n">size</span><span class="p" data-group-id="6257658676-92">(</span><span class="mi">8</span><span class="p" data-group-id="6257658676-92">)</span><span class="p">,</span><span class="w"> </span><span class="n">g2</span><span class="o">::</span><span class="n">binary</span><span class="o">-</span><span class="n">size</span><span class="p" data-group-id="6257658676-93">(</span><span class="mi">4</span><span class="p" data-group-id="6257658676-93">)</span><span class="p">,</span><span class="w"> </span><span class="n">g3</span><span class="o">::</span><span class="n">binary</span><span class="o">-</span><span class="n">size</span><span class="p" data-group-id="6257658676-94">(</span><span class="mi">4</span><span class="p" data-group-id="6257658676-94">)</span><span class="p">,</span><span class="w"> </span><span class="n">g4</span><span class="o">::</span><span class="n">binary</span><span class="o">-</span><span class="n">size</span><span class="p" data-group-id="6257658676-95">(</span><span class="mi">4</span><span class="p" data-group-id="6257658676-95">)</span><span class="p">,</span><span class="w"> </span><span class="n">g5</span><span class="o">::</span><span class="n">binary</span><span class="o">-</span><span class="n">size</span><span class="p" data-group-id="6257658676-96">(</span><span class="mi">12</span><span class="p" data-group-id="6257658676-96">)</span><span class="p" data-group-id="6257658676-91">&gt;&gt;</span><span class="w"> </span><span class="o">-&gt;</span><span class="w">
        </span><span class="p" data-group-id="6257658676-97">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="s">&quot;</span><span class="si" data-group-id="6257658676-98">#{</span><span class="n">g1</span><span class="si" data-group-id="6257658676-98">}</span><span class="s">-</span><span class="si" data-group-id="6257658676-99">#{</span><span class="n">g2</span><span class="si" data-group-id="6257658676-99">}</span><span class="s">-</span><span class="si" data-group-id="6257658676-100">#{</span><span class="n">g3</span><span class="si" data-group-id="6257658676-100">}</span><span class="s">-</span><span class="si" data-group-id="6257658676-101">#{</span><span class="n">g4</span><span class="si" data-group-id="6257658676-101">}</span><span class="s">-</span><span class="si" data-group-id="6257658676-102">#{</span><span class="n">g5</span><span class="si" data-group-id="6257658676-102">}</span><span class="s">&quot;</span><span class="p" data-group-id="6257658676-97">}</span><span class="w">

      </span><span class="n">other</span><span class="w"> </span><span class="o">-&gt;</span><span class="w">
        </span><span class="p" data-group-id="6257658676-103">{</span><span class="ss">:error</span><span class="p">,</span><span class="w"> </span><span class="s">&quot;got invalid base62 uuid; </span><span class="si" data-group-id="6257658676-104">#{</span><span class="n">inspect</span><span class="w"> </span><span class="n">other</span><span class="si" data-group-id="6257658676-104">}</span><span class="s">&quot;</span><span class="p" data-group-id="6257658676-103">}</span><span class="w">
    </span><span class="k" data-group-id="6257658676-90">end</span><span class="w">
  </span><span class="k" data-group-id="6257658676-86">end</span><span class="w">

  </span><span class="c1"># Base62 encoder/decoder</span><span class="w">

  </span><span class="na">@base62_alphabet</span><span class="w"> </span><span class="sc">&#39;0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz&#39;</span><span class="w">

  </span><span class="k">for</span><span class="w"> </span><span class="p" data-group-id="6257658676-105">{</span><span class="n">digit</span><span class="p">,</span><span class="w"> </span><span class="n">idx</span><span class="p" data-group-id="6257658676-105">}</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="nc">Enum</span><span class="o">.</span><span class="n">with_index</span><span class="p" data-group-id="6257658676-106">(</span><span class="na">@base62_alphabet</span><span class="p" data-group-id="6257658676-106">)</span><span class="w"> </span><span class="k" data-group-id="6257658676-107">do</span><span class="w">
    </span><span class="kd">defp</span><span class="w"> </span><span class="nf">base62_encode</span><span class="p" data-group-id="6257658676-108">(</span><span class="k">unquote</span><span class="p" data-group-id="6257658676-109">(</span><span class="n">idx</span><span class="p" data-group-id="6257658676-109">)</span><span class="p" data-group-id="6257658676-108">)</span><span class="p">,</span><span class="w"> </span><span class="ss">do</span><span class="p">:</span><span class="w"> </span><span class="k">unquote</span><span class="p" data-group-id="6257658676-110">(</span><span class="p" data-group-id="6257658676-111">&lt;&lt;</span><span class="n">digit</span><span class="p" data-group-id="6257658676-111">&gt;&gt;</span><span class="p" data-group-id="6257658676-110">)</span><span class="w">
  </span><span class="k" data-group-id="6257658676-107">end</span><span class="w">

  </span><span class="kd">defp</span><span class="w"> </span><span class="nf">base62_encode</span><span class="p" data-group-id="6257658676-112">(</span><span class="n">number</span><span class="p" data-group-id="6257658676-112">)</span><span class="w"> </span><span class="k" data-group-id="6257658676-113">do</span><span class="w">
    </span><span class="n">base62_encode</span><span class="p" data-group-id="6257658676-114">(</span><span class="n">div</span><span class="p" data-group-id="6257658676-115">(</span><span class="n">number</span><span class="p">,</span><span class="w"> </span><span class="k">unquote</span><span class="p" data-group-id="6257658676-116">(</span><span class="n">length</span><span class="p" data-group-id="6257658676-117">(</span><span class="na">@base62_alphabet</span><span class="p" data-group-id="6257658676-117">)</span><span class="p" data-group-id="6257658676-116">)</span><span class="p" data-group-id="6257658676-115">)</span><span class="p" data-group-id="6257658676-114">)</span><span class="w"> </span><span class="o">&lt;&gt;</span><span class="w">
      </span><span class="n">base62_encode</span><span class="p" data-group-id="6257658676-118">(</span><span class="n">rem</span><span class="p" data-group-id="6257658676-119">(</span><span class="n">number</span><span class="p">,</span><span class="w"> </span><span class="k">unquote</span><span class="p" data-group-id="6257658676-120">(</span><span class="n">length</span><span class="p" data-group-id="6257658676-121">(</span><span class="na">@base62_alphabet</span><span class="p" data-group-id="6257658676-121">)</span><span class="p" data-group-id="6257658676-120">)</span><span class="p" data-group-id="6257658676-119">)</span><span class="p" data-group-id="6257658676-118">)</span><span class="w">
  </span><span class="k" data-group-id="6257658676-113">end</span><span class="w">

  </span><span class="kd">defp</span><span class="w"> </span><span class="nf">base62_decode</span><span class="p" data-group-id="6257658676-122">(</span><span class="n">string</span><span class="p" data-group-id="6257658676-122">)</span><span class="w"> </span><span class="k" data-group-id="6257658676-123">do</span><span class="w">
    </span><span class="n">string</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="nc">String</span><span class="o">.</span><span class="n">split</span><span class="p" data-group-id="6257658676-124">(</span><span class="s">&quot;&quot;</span><span class="p">,</span><span class="w"> </span><span class="ss">trim</span><span class="p">:</span><span class="w"> </span><span class="no">true</span><span class="p" data-group-id="6257658676-124">)</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="nc">Enum</span><span class="o">.</span><span class="n">reverse</span><span class="p" data-group-id="6257658676-125">(</span><span class="p" data-group-id="6257658676-125">)</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="nc">Enum</span><span class="o">.</span><span class="n">reduce_while</span><span class="p" data-group-id="6257658676-126">(</span><span class="p" data-group-id="6257658676-127">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="6257658676-128">{</span><span class="mi">0</span><span class="p">,</span><span class="w"> </span><span class="mi">0</span><span class="p" data-group-id="6257658676-128">}</span><span class="p" data-group-id="6257658676-127">}</span><span class="p">,</span><span class="w"> </span><span class="k" data-group-id="6257658676-129">fn</span><span class="w"> </span><span class="n">char</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="6257658676-130">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="6257658676-131">{</span><span class="n">acc</span><span class="p">,</span><span class="w"> </span><span class="n">step</span><span class="p" data-group-id="6257658676-131">}</span><span class="p" data-group-id="6257658676-130">}</span><span class="w"> </span><span class="o">-&gt;</span><span class="w">
      </span><span class="k">case</span><span class="w"> </span><span class="n">decode_base62_char</span><span class="p" data-group-id="6257658676-132">(</span><span class="n">char</span><span class="p" data-group-id="6257658676-132">)</span><span class="w"> </span><span class="k" data-group-id="6257658676-133">do</span><span class="w">
        </span><span class="p" data-group-id="6257658676-134">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="n">number</span><span class="p" data-group-id="6257658676-134">}</span><span class="w"> </span><span class="o">-&gt;</span><span class="w"> </span><span class="p" data-group-id="6257658676-135">{</span><span class="ss">:cont</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="6257658676-136">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="6257658676-137">{</span><span class="n">acc</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="n">number</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="nc">Integer</span><span class="o">.</span><span class="n">pow</span><span class="p" data-group-id="6257658676-138">(</span><span class="k">unquote</span><span class="p" data-group-id="6257658676-139">(</span><span class="n">length</span><span class="p" data-group-id="6257658676-140">(</span><span class="na">@base62_alphabet</span><span class="p" data-group-id="6257658676-140">)</span><span class="p" data-group-id="6257658676-139">)</span><span class="p">,</span><span class="w"> </span><span class="n">step</span><span class="p" data-group-id="6257658676-138">)</span><span class="p">,</span><span class="w"> </span><span class="n">step</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="mi">1</span><span class="p" data-group-id="6257658676-137">}</span><span class="p" data-group-id="6257658676-136">}</span><span class="p" data-group-id="6257658676-135">}</span><span class="w">
        </span><span class="p" data-group-id="6257658676-141">{</span><span class="ss">:error</span><span class="p">,</span><span class="w"> </span><span class="n">error</span><span class="p" data-group-id="6257658676-141">}</span><span class="w"> </span><span class="o">-&gt;</span><span class="w"> </span><span class="p" data-group-id="6257658676-142">{</span><span class="ss">:halt</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="6257658676-143">{</span><span class="ss">:error</span><span class="p">,</span><span class="w"> </span><span class="n">error</span><span class="p" data-group-id="6257658676-143">}</span><span class="p" data-group-id="6257658676-142">}</span><span class="w">
      </span><span class="k" data-group-id="6257658676-133">end</span><span class="w">
    </span><span class="k" data-group-id="6257658676-129">end</span><span class="p" data-group-id="6257658676-126">)</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="k">case</span><span class="w"> </span><span class="k" data-group-id="6257658676-144">do</span><span class="w">
      </span><span class="p" data-group-id="6257658676-145">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="6257658676-146">{</span><span class="n">number</span><span class="p">,</span><span class="w"> </span><span class="c">_step</span><span class="p" data-group-id="6257658676-146">}</span><span class="p" data-group-id="6257658676-145">}</span><span class="w"> </span><span class="o">-&gt;</span><span class="w"> </span><span class="p" data-group-id="6257658676-147">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="n">number</span><span class="p" data-group-id="6257658676-147">}</span><span class="w">
      </span><span class="p" data-group-id="6257658676-148">{</span><span class="ss">:error</span><span class="p">,</span><span class="w"> </span><span class="n">error</span><span class="p" data-group-id="6257658676-148">}</span><span class="w"> </span><span class="o">-&gt;</span><span class="w"> </span><span class="p" data-group-id="6257658676-149">{</span><span class="ss">:error</span><span class="p">,</span><span class="w"> </span><span class="n">error</span><span class="p" data-group-id="6257658676-149">}</span><span class="w">
    </span><span class="k" data-group-id="6257658676-144">end</span><span class="w">
  </span><span class="k" data-group-id="6257658676-123">end</span><span class="w">

  </span><span class="k">for</span><span class="w"> </span><span class="p" data-group-id="6257658676-150">{</span><span class="n">digit</span><span class="p">,</span><span class="w"> </span><span class="n">idx</span><span class="p" data-group-id="6257658676-150">}</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="nc">Enum</span><span class="o">.</span><span class="n">with_index</span><span class="p" data-group-id="6257658676-151">(</span><span class="na">@base62_alphabet</span><span class="p" data-group-id="6257658676-151">)</span><span class="w"> </span><span class="k" data-group-id="6257658676-152">do</span><span class="w">
    </span><span class="kd">defp</span><span class="w"> </span><span class="nf">decode_base62_char</span><span class="p" data-group-id="6257658676-153">(</span><span class="k">unquote</span><span class="p" data-group-id="6257658676-154">(</span><span class="p" data-group-id="6257658676-155">&lt;&lt;</span><span class="n">digit</span><span class="p" data-group-id="6257658676-155">&gt;&gt;</span><span class="p" data-group-id="6257658676-154">)</span><span class="p" data-group-id="6257658676-153">)</span><span class="p">,</span><span class="w"> </span><span class="ss">do</span><span class="p">:</span><span class="w"> </span><span class="p" data-group-id="6257658676-156">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="k">unquote</span><span class="p" data-group-id="6257658676-157">(</span><span class="n">idx</span><span class="p" data-group-id="6257658676-157">)</span><span class="p" data-group-id="6257658676-156">}</span><span class="w">
  </span><span class="k" data-group-id="6257658676-152">end</span><span class="w">

  </span><span class="kd">defp</span><span class="w"> </span><span class="nf">decode_base62_char</span><span class="p" data-group-id="6257658676-158">(</span><span class="n">char</span><span class="p" data-group-id="6257658676-158">)</span><span class="p">,</span><span class="w"> </span><span class="ss">do</span><span class="p">:</span><span class="w"> </span><span class="p" data-group-id="6257658676-159">{</span><span class="ss">:error</span><span class="p">,</span><span class="w"> </span><span class="s">&quot;got invalid base62 character; </span><span class="si" data-group-id="6257658676-160">#{</span><span class="n">inspect</span><span class="w"> </span><span class="n">char</span><span class="si" data-group-id="6257658676-160">}</span><span class="s">&quot;</span><span class="p" data-group-id="6257658676-159">}</span><span class="w">
</span><span class="k" data-group-id="6257658676-1">end</span></code></pre>
<pre><code class="makeup elixir"><span class="kd">defmodule</span><span class="w"> </span><span class="nc">MyApp.PrefixedUUIDTest</span><span class="w"> </span><span class="k" data-group-id="1133397618-1">do</span><span class="w">
  </span><span class="kn">use</span><span class="w"> </span><span class="nc">MyApp.DataCase</span><span class="w">

  </span><span class="kn">alias</span><span class="w"> </span><span class="nc">MyApp.PrefixedUUID</span><span class="w">
  </span><span class="kn">alias</span><span class="w"> </span><span class="nc">Uniq.UUID</span><span class="w">

  </span><span class="kd">defmodule</span><span class="w"> </span><span class="nc">TestSchema</span><span class="w"> </span><span class="k" data-group-id="1133397618-2">do</span><span class="w">
    </span><span class="kn">use</span><span class="w"> </span><span class="nc">Ecto.Schema</span><span class="w">

    </span><span class="na">@primary_key</span><span class="w"> </span><span class="p" data-group-id="1133397618-3">{</span><span class="ss">:id</span><span class="p">,</span><span class="w"> </span><span class="nc">PrefixedUUID</span><span class="p">,</span><span class="w"> </span><span class="ss">prefix</span><span class="p">:</span><span class="w"> </span><span class="s">&quot;test&quot;</span><span class="p">,</span><span class="w"> </span><span class="ss">autogenerate</span><span class="p">:</span><span class="w"> </span><span class="no">true</span><span class="p" data-group-id="1133397618-3">}</span><span class="w">
    </span><span class="na">@foreign_key_type</span><span class="w"> </span><span class="nc">PrefixedUUID</span><span class="w">

    </span><span class="n">schema</span><span class="w"> </span><span class="s">&quot;test&quot;</span><span class="w"> </span><span class="k" data-group-id="1133397618-4">do</span><span class="w">
      </span><span class="n">belongs_to</span><span class="w"> </span><span class="ss">:test</span><span class="p">,</span><span class="w"> </span><span class="nc">TestSchema</span><span class="w">
    </span><span class="k" data-group-id="1133397618-4">end</span><span class="w">
  </span><span class="k" data-group-id="1133397618-2">end</span><span class="w">

  </span><span class="na">@params</span><span class="w"> </span><span class="nc">PrefixedUUID</span><span class="o">.</span><span class="n">init</span><span class="p" data-group-id="1133397618-5">(</span><span class="ss">schema</span><span class="p">:</span><span class="w"> </span><span class="nc">TestSchema</span><span class="p">,</span><span class="w"> </span><span class="ss">field</span><span class="p">:</span><span class="w"> </span><span class="ss">:id</span><span class="p">,</span><span class="w"> </span><span class="ss">primary_key</span><span class="p">:</span><span class="w"> </span><span class="no">true</span><span class="p">,</span><span class="w"> </span><span class="ss">autogenerate</span><span class="p">:</span><span class="w"> </span><span class="no">true</span><span class="p">,</span><span class="w"> </span><span class="ss">prefix</span><span class="p">:</span><span class="w"> </span><span class="s">&quot;test&quot;</span><span class="p" data-group-id="1133397618-5">)</span><span class="w">
  </span><span class="na">@belongs_to_params</span><span class="w"> </span><span class="nc">PrefixedUUID</span><span class="o">.</span><span class="n">init</span><span class="p" data-group-id="1133397618-6">(</span><span class="ss">schema</span><span class="p">:</span><span class="w"> </span><span class="nc">TestSchema</span><span class="p">,</span><span class="w"> </span><span class="ss">field</span><span class="p">:</span><span class="w"> </span><span class="ss">:test</span><span class="p">,</span><span class="w"> </span><span class="ss">foreign_key</span><span class="p">:</span><span class="w"> </span><span class="ss">:test_id</span><span class="p" data-group-id="1133397618-6">)</span><span class="w">
  </span><span class="na">@loader</span><span class="w"> </span><span class="no">nil</span><span class="w">
  </span><span class="na">@dumper</span><span class="w"> </span><span class="no">nil</span><span class="w">

  </span><span class="na">@test_prefixed_uuid</span><span class="w"> </span><span class="s">&quot;test_3TUIKuXX5mNO2jSA41bsDx&quot;</span><span class="w">
  </span><span class="na">@test_uuid</span><span class="w"> </span><span class="nc">UUID</span><span class="o">.</span><span class="n">to_string</span><span class="p" data-group-id="1133397618-7">(</span><span class="s">&quot;7232b37d-fc13-44c0-8e1b-9a5a07e24921&quot;</span><span class="p">,</span><span class="w"> </span><span class="ss">:raw</span><span class="p" data-group-id="1133397618-7">)</span><span class="w">
  </span><span class="na">@test_prefixed_uuid_with_leading_zero</span><span class="w"> </span><span class="s">&quot;test_02tREKF6r6OCO2sdSjpyTm&quot;</span><span class="w">
  </span><span class="na">@test_uuid_with_leading_zero</span><span class="w"> </span><span class="nc">UUID</span><span class="o">.</span><span class="n">to_string</span><span class="p" data-group-id="1133397618-8">(</span><span class="s">&quot;0188a516-bc8c-7c5a-9b68-12651f558b9e&quot;</span><span class="p">,</span><span class="w"> </span><span class="ss">:raw</span><span class="p" data-group-id="1133397618-8">)</span><span class="w">
  </span><span class="na">@test_prefixed_uuid_null</span><span class="w"> </span><span class="s">&quot;test_0000000000000000000000&quot;</span><span class="w">
  </span><span class="na">@test_uuid_null</span><span class="w"> </span><span class="nc">UUID</span><span class="o">.</span><span class="n">to_string</span><span class="p" data-group-id="1133397618-9">(</span><span class="s">&quot;00000000-0000-0000-0000-000000000000&quot;</span><span class="p">,</span><span class="w"> </span><span class="ss">:raw</span><span class="p" data-group-id="1133397618-9">)</span><span class="w">
  </span><span class="na">@test_prefixed_uuid_invalid_characters</span><span class="w"> </span><span class="s">&quot;test_&quot;</span><span class="w"> </span><span class="o">&lt;&gt;</span><span class="w"> </span><span class="nc">String</span><span class="o">.</span><span class="n">duplicate</span><span class="p" data-group-id="1133397618-10">(</span><span class="s">&quot;.&quot;</span><span class="p">,</span><span class="w"> </span><span class="mi">32</span><span class="p" data-group-id="1133397618-10">)</span><span class="w">
  </span><span class="na">@test_uuid_invalid_characters</span><span class="w"> </span><span class="nc">String</span><span class="o">.</span><span class="n">duplicate</span><span class="p" data-group-id="1133397618-11">(</span><span class="s">&quot;.&quot;</span><span class="p">,</span><span class="w"> </span><span class="mi">22</span><span class="p" data-group-id="1133397618-11">)</span><span class="w">
  </span><span class="na">@test_prefixed_uuid_invalid_format</span><span class="w"> </span><span class="s">&quot;test_&quot;</span><span class="w"> </span><span class="o">&lt;&gt;</span><span class="w"> </span><span class="nc">String</span><span class="o">.</span><span class="n">duplicate</span><span class="p" data-group-id="1133397618-12">(</span><span class="s">&quot;x&quot;</span><span class="p">,</span><span class="w"> </span><span class="mi">31</span><span class="p" data-group-id="1133397618-12">)</span><span class="w">
  </span><span class="na">@test_uuid_invalid_format</span><span class="w"> </span><span class="nc">String</span><span class="o">.</span><span class="n">duplicate</span><span class="p" data-group-id="1133397618-13">(</span><span class="s">&quot;x&quot;</span><span class="p">,</span><span class="w"> </span><span class="mi">21</span><span class="p" data-group-id="1133397618-13">)</span><span class="w">

  </span><span class="n">test</span><span class="w"> </span><span class="s">&quot;cast/2&quot;</span><span class="w"> </span><span class="k" data-group-id="1133397618-14">do</span><span class="w">
    </span><span class="n">assert</span><span class="w"> </span><span class="nc">PrefixedUUID</span><span class="o">.</span><span class="n">cast</span><span class="p" data-group-id="1133397618-15">(</span><span class="na">@test_prefixed_uuid</span><span class="p">,</span><span class="w"> </span><span class="na">@params</span><span class="p" data-group-id="1133397618-15">)</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="p" data-group-id="1133397618-16">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="na">@test_prefixed_uuid</span><span class="p" data-group-id="1133397618-16">}</span><span class="w">
    </span><span class="n">assert</span><span class="w"> </span><span class="nc">PrefixedUUID</span><span class="o">.</span><span class="n">cast</span><span class="p" data-group-id="1133397618-17">(</span><span class="na">@test_prefixed_uuid_with_leading_zero</span><span class="p">,</span><span class="w"> </span><span class="na">@params</span><span class="p" data-group-id="1133397618-17">)</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="p" data-group-id="1133397618-18">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="na">@test_prefixed_uuid_with_leading_zero</span><span class="p" data-group-id="1133397618-18">}</span><span class="w">
    </span><span class="n">assert</span><span class="w"> </span><span class="nc">PrefixedUUID</span><span class="o">.</span><span class="n">cast</span><span class="p" data-group-id="1133397618-19">(</span><span class="na">@test_prefixed_uuid_null</span><span class="p">,</span><span class="w"> </span><span class="na">@params</span><span class="p" data-group-id="1133397618-19">)</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="p" data-group-id="1133397618-20">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="na">@test_prefixed_uuid_null</span><span class="p" data-group-id="1133397618-20">}</span><span class="w">
    </span><span class="n">assert</span><span class="w"> </span><span class="nc">PrefixedUUID</span><span class="o">.</span><span class="n">cast</span><span class="p" data-group-id="1133397618-21">(</span><span class="no">nil</span><span class="p">,</span><span class="w"> </span><span class="na">@params</span><span class="p" data-group-id="1133397618-21">)</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="p" data-group-id="1133397618-22">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="no">nil</span><span class="p" data-group-id="1133397618-22">}</span><span class="w">
    </span><span class="n">assert</span><span class="w"> </span><span class="nc">PrefixedUUID</span><span class="o">.</span><span class="n">cast</span><span class="p" data-group-id="1133397618-23">(</span><span class="s">&quot;otherprefix&quot;</span><span class="w"> </span><span class="o">&lt;&gt;</span><span class="w"> </span><span class="na">@test_prefixed_uuid</span><span class="p">,</span><span class="w"> </span><span class="na">@params</span><span class="p" data-group-id="1133397618-23">)</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="ss">:error</span><span class="w">
    </span><span class="n">assert</span><span class="w"> </span><span class="nc">PrefixedUUID</span><span class="o">.</span><span class="n">cast</span><span class="p" data-group-id="1133397618-24">(</span><span class="na">@test_prefixed_uuid_invalid_characters</span><span class="p">,</span><span class="w"> </span><span class="na">@params</span><span class="p" data-group-id="1133397618-24">)</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="ss">:error</span><span class="w">
    </span><span class="n">assert</span><span class="w"> </span><span class="nc">PrefixedUUID</span><span class="o">.</span><span class="n">cast</span><span class="p" data-group-id="1133397618-25">(</span><span class="na">@test_prefixed_uuid_invalid_format</span><span class="p">,</span><span class="w"> </span><span class="na">@params</span><span class="p" data-group-id="1133397618-25">)</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="ss">:error</span><span class="w">
    </span><span class="n">assert</span><span class="w"> </span><span class="nc">PrefixedUUID</span><span class="o">.</span><span class="n">cast</span><span class="p" data-group-id="1133397618-26">(</span><span class="na">@test_prefixed_uuid</span><span class="p">,</span><span class="w"> </span><span class="na">@belongs_to_params</span><span class="p" data-group-id="1133397618-26">)</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="p" data-group-id="1133397618-27">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="na">@test_prefixed_uuid</span><span class="p" data-group-id="1133397618-27">}</span><span class="w">
  </span><span class="k" data-group-id="1133397618-14">end</span><span class="w">

  </span><span class="n">test</span><span class="w"> </span><span class="s">&quot;load/3&quot;</span><span class="w"> </span><span class="k" data-group-id="1133397618-28">do</span><span class="w">
    </span><span class="n">assert</span><span class="w"> </span><span class="nc">PrefixedUUID</span><span class="o">.</span><span class="n">load</span><span class="p" data-group-id="1133397618-29">(</span><span class="na">@test_uuid</span><span class="p">,</span><span class="w"> </span><span class="na">@loader</span><span class="p">,</span><span class="w"> </span><span class="na">@params</span><span class="p" data-group-id="1133397618-29">)</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="p" data-group-id="1133397618-30">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="na">@test_prefixed_uuid</span><span class="p" data-group-id="1133397618-30">}</span><span class="w">
    </span><span class="n">assert</span><span class="w"> </span><span class="nc">PrefixedUUID</span><span class="o">.</span><span class="n">load</span><span class="p" data-group-id="1133397618-31">(</span><span class="na">@test_uuid_with_leading_zero</span><span class="p">,</span><span class="w"> </span><span class="na">@loader</span><span class="p">,</span><span class="w"> </span><span class="na">@params</span><span class="p" data-group-id="1133397618-31">)</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="p" data-group-id="1133397618-32">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="na">@test_prefixed_uuid_with_leading_zero</span><span class="p" data-group-id="1133397618-32">}</span><span class="w">
    </span><span class="n">assert</span><span class="w"> </span><span class="nc">PrefixedUUID</span><span class="o">.</span><span class="n">load</span><span class="p" data-group-id="1133397618-33">(</span><span class="na">@test_uuid_null</span><span class="p">,</span><span class="w"> </span><span class="na">@loader</span><span class="p">,</span><span class="w"> </span><span class="na">@params</span><span class="p" data-group-id="1133397618-33">)</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="p" data-group-id="1133397618-34">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="na">@test_prefixed_uuid_null</span><span class="p" data-group-id="1133397618-34">}</span><span class="w">
    </span><span class="n">assert</span><span class="w"> </span><span class="nc">PrefixedUUID</span><span class="o">.</span><span class="n">load</span><span class="p" data-group-id="1133397618-35">(</span><span class="na">@test_uuid_invalid_characters</span><span class="p">,</span><span class="w"> </span><span class="na">@loader</span><span class="p">,</span><span class="w"> </span><span class="na">@params</span><span class="p" data-group-id="1133397618-35">)</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="ss">:error</span><span class="w">
    </span><span class="n">assert</span><span class="w"> </span><span class="nc">PrefixedUUID</span><span class="o">.</span><span class="n">load</span><span class="p" data-group-id="1133397618-36">(</span><span class="na">@test_uuid_invalid_format</span><span class="p">,</span><span class="w"> </span><span class="na">@loader</span><span class="p">,</span><span class="w"> </span><span class="na">@params</span><span class="p" data-group-id="1133397618-36">)</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="ss">:error</span><span class="w">
    </span><span class="n">assert</span><span class="w"> </span><span class="nc">PrefixedUUID</span><span class="o">.</span><span class="n">load</span><span class="p" data-group-id="1133397618-37">(</span><span class="na">@test_prefixed_uuid</span><span class="p">,</span><span class="w"> </span><span class="na">@loader</span><span class="p">,</span><span class="w"> </span><span class="na">@params</span><span class="p" data-group-id="1133397618-37">)</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="ss">:error</span><span class="w">
    </span><span class="n">assert</span><span class="w"> </span><span class="nc">PrefixedUUID</span><span class="o">.</span><span class="n">load</span><span class="p" data-group-id="1133397618-38">(</span><span class="no">nil</span><span class="p">,</span><span class="w"> </span><span class="na">@loader</span><span class="p">,</span><span class="w"> </span><span class="na">@params</span><span class="p" data-group-id="1133397618-38">)</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="p" data-group-id="1133397618-39">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="no">nil</span><span class="p" data-group-id="1133397618-39">}</span><span class="w">
    </span><span class="n">assert</span><span class="w"> </span><span class="nc">PrefixedUUID</span><span class="o">.</span><span class="n">load</span><span class="p" data-group-id="1133397618-40">(</span><span class="na">@test_uuid</span><span class="p">,</span><span class="w"> </span><span class="na">@loader</span><span class="p">,</span><span class="w"> </span><span class="na">@belongs_to_params</span><span class="p" data-group-id="1133397618-40">)</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="p" data-group-id="1133397618-41">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="na">@test_prefixed_uuid</span><span class="p" data-group-id="1133397618-41">}</span><span class="w">
  </span><span class="k" data-group-id="1133397618-28">end</span><span class="w">

  </span><span class="n">test</span><span class="w"> </span><span class="s">&quot;dump/3&quot;</span><span class="w"> </span><span class="k" data-group-id="1133397618-42">do</span><span class="w">
    </span><span class="n">assert</span><span class="w"> </span><span class="nc">PrefixedUUID</span><span class="o">.</span><span class="n">dump</span><span class="p" data-group-id="1133397618-43">(</span><span class="na">@test_prefixed_uuid</span><span class="p">,</span><span class="w"> </span><span class="na">@dumper</span><span class="p">,</span><span class="w"> </span><span class="na">@params</span><span class="p" data-group-id="1133397618-43">)</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="p" data-group-id="1133397618-44">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="na">@test_uuid</span><span class="p" data-group-id="1133397618-44">}</span><span class="w">
    </span><span class="n">assert</span><span class="w"> </span><span class="nc">PrefixedUUID</span><span class="o">.</span><span class="n">dump</span><span class="p" data-group-id="1133397618-45">(</span><span class="na">@test_prefixed_uuid_with_leading_zero</span><span class="p">,</span><span class="w"> </span><span class="na">@dumper</span><span class="p">,</span><span class="w"> </span><span class="na">@params</span><span class="p" data-group-id="1133397618-45">)</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="p" data-group-id="1133397618-46">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="na">@test_uuid_with_leading_zero</span><span class="p" data-group-id="1133397618-46">}</span><span class="w">
    </span><span class="n">assert</span><span class="w"> </span><span class="nc">PrefixedUUID</span><span class="o">.</span><span class="n">dump</span><span class="p" data-group-id="1133397618-47">(</span><span class="na">@test_prefixed_uuid_null</span><span class="p">,</span><span class="w"> </span><span class="na">@dumper</span><span class="p">,</span><span class="w"> </span><span class="na">@params</span><span class="p" data-group-id="1133397618-47">)</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="p" data-group-id="1133397618-48">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="na">@test_uuid_null</span><span class="p" data-group-id="1133397618-48">}</span><span class="w">
    </span><span class="n">assert</span><span class="w"> </span><span class="nc">PrefixedUUID</span><span class="o">.</span><span class="n">dump</span><span class="p" data-group-id="1133397618-49">(</span><span class="na">@test_uuid</span><span class="p">,</span><span class="w"> </span><span class="na">@dumper</span><span class="p">,</span><span class="w"> </span><span class="na">@params</span><span class="p" data-group-id="1133397618-49">)</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="ss">:error</span><span class="w">
    </span><span class="n">assert</span><span class="w"> </span><span class="nc">PrefixedUUID</span><span class="o">.</span><span class="n">dump</span><span class="p" data-group-id="1133397618-50">(</span><span class="no">nil</span><span class="p">,</span><span class="w"> </span><span class="na">@dumper</span><span class="p">,</span><span class="w"> </span><span class="na">@params</span><span class="p" data-group-id="1133397618-50">)</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="p" data-group-id="1133397618-51">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="no">nil</span><span class="p" data-group-id="1133397618-51">}</span><span class="w">
    </span><span class="n">assert</span><span class="w"> </span><span class="nc">PrefixedUUID</span><span class="o">.</span><span class="n">dump</span><span class="p" data-group-id="1133397618-52">(</span><span class="na">@test_prefixed_uuid</span><span class="p">,</span><span class="w"> </span><span class="na">@dumper</span><span class="p">,</span><span class="w"> </span><span class="na">@belongs_to_params</span><span class="p" data-group-id="1133397618-52">)</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="p" data-group-id="1133397618-53">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="na">@test_uuid</span><span class="p" data-group-id="1133397618-53">}</span><span class="w">
  </span><span class="k" data-group-id="1133397618-42">end</span><span class="w">

  </span><span class="n">test</span><span class="w"> </span><span class="s">&quot;autogenerate/1&quot;</span><span class="w"> </span><span class="k" data-group-id="1133397618-54">do</span><span class="w">
    </span><span class="n">assert</span><span class="w"> </span><span class="n">prefixed_uuid</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">PrefixedUUID</span><span class="o">.</span><span class="n">autogenerate</span><span class="p" data-group-id="1133397618-55">(</span><span class="na">@params</span><span class="p" data-group-id="1133397618-55">)</span><span class="w">
    </span><span class="n">assert</span><span class="w"> </span><span class="p" data-group-id="1133397618-56">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="n">uuid</span><span class="p" data-group-id="1133397618-56">}</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">PrefixedUUID</span><span class="o">.</span><span class="n">dump</span><span class="p" data-group-id="1133397618-57">(</span><span class="n">prefixed_uuid</span><span class="p">,</span><span class="w"> </span><span class="no">nil</span><span class="p">,</span><span class="w"> </span><span class="na">@params</span><span class="p" data-group-id="1133397618-57">)</span><span class="w">
    </span><span class="n">assert</span><span class="w"> </span><span class="p" data-group-id="1133397618-58">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="1133397618-59">%</span><span class="nc" data-group-id="1133397618-59">UUID</span><span class="p" data-group-id="1133397618-59">{</span><span class="ss">format</span><span class="p">:</span><span class="w"> </span><span class="ss">:raw</span><span class="p">,</span><span class="w"> </span><span class="ss">version</span><span class="p">:</span><span class="w"> </span><span class="mi">7</span><span class="p" data-group-id="1133397618-59">}</span><span class="p" data-group-id="1133397618-58">}</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">UUID</span><span class="o">.</span><span class="n">parse</span><span class="p" data-group-id="1133397618-60">(</span><span class="n">uuid</span><span class="p" data-group-id="1133397618-60">)</span><span class="w">
  </span><span class="k" data-group-id="1133397618-54">end</span><span class="w">
</span><span class="k" data-group-id="1133397618-1">end</span></code></pre>
<p>
With this we can set up our schema module key types:</p>
<pre><code class="makeup elixir"><span class="na">@primary_key</span><span class="w"> </span><span class="p" data-group-id="9849916442-1">{</span><span class="ss">:id</span><span class="p">,</span><span class="w"> </span><span class="nc">PrefixedUUID</span><span class="p">,</span><span class="w"> </span><span class="ss">prefix</span><span class="p">:</span><span class="w"> </span><span class="s">&quot;test&quot;</span><span class="p">,</span><span class="w"> </span><span class="ss">autogenerate</span><span class="p">:</span><span class="w"> </span><span class="no">true</span><span class="p" data-group-id="9849916442-1">}</span><span class="w">
</span><span class="na">@foreign_key_type</span><span class="w"> </span><span class="nc">PrefixedUUID</span></code></pre>
<p>
We can make it more DRY by setting up and using a <code class="inline">MyApp.Schema</code> module instead of <code class="inline">Ecto.Schema</code> in our schema modules:</p>
<pre><code class="makeup elixir"><span class="kd">defmodule</span><span class="w"> </span><span class="nc">MyApp.Schema</span><span class="w"> </span><span class="k" data-group-id="1307432362-1">do</span><span class="w">
  </span><span class="na">@moduledoc</span><span class="w"> </span><span class="s">&quot;&quot;&quot;
  This module defines the default schema settings for schema modules
  in MyApp.

  Primary and foreign keys are prefixed UUIDs.

  ## Usage

      defmodule MyApp.Accounts.Account do
        use MyApp.Schema, prefix: &quot;acct&quot;

        # ...
      end
  &quot;&quot;&quot;</span><span class="w">

  </span><span class="kd">defmacro</span><span class="w"> </span><span class="nf">__using__</span><span class="p" data-group-id="1307432362-2">(</span><span class="n">opts</span><span class="w"> </span><span class="o">\\</span><span class="w"> </span><span class="p" data-group-id="1307432362-3">[</span><span class="p" data-group-id="1307432362-3">]</span><span class="p" data-group-id="1307432362-2">)</span><span class="w"> </span><span class="k" data-group-id="1307432362-4">do</span><span class="w">
    </span><span class="n">prefix</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">Keyword</span><span class="o">.</span><span class="n">fetch!</span><span class="p" data-group-id="1307432362-5">(</span><span class="n">opts</span><span class="p">,</span><span class="w"> </span><span class="ss">:prefix</span><span class="p" data-group-id="1307432362-5">)</span><span class="w">

    </span><span class="k">quote</span><span class="w"> </span><span class="k" data-group-id="1307432362-6">do</span><span class="w">
      </span><span class="kn">use</span><span class="w"> </span><span class="nc">Ecto.Schema</span><span class="w">

      </span><span class="na">@primary_key</span><span class="w"> </span><span class="p" data-group-id="1307432362-7">{</span><span class="ss">:id</span><span class="p">,</span><span class="w"> </span><span class="nc">MyApp.PrefixedUUID</span><span class="p">,</span><span class="w"> </span><span class="ss">prefix</span><span class="p">:</span><span class="w"> </span><span class="k">unquote</span><span class="p" data-group-id="1307432362-8">(</span><span class="n">prefix</span><span class="p" data-group-id="1307432362-8">)</span><span class="p">,</span><span class="w"> </span><span class="ss">autogenerate</span><span class="p">:</span><span class="w"> </span><span class="no">true</span><span class="p" data-group-id="1307432362-7">}</span><span class="w">
      </span><span class="na">@foreign_key_type</span><span class="w"> </span><span class="nc">MyApp.PrefixedUUID</span><span class="w">

      </span><span class="na">@type</span><span class="w"> </span><span class="n">t</span><span class="w"> </span><span class="o">::</span><span class="w"> </span><span class="p">%</span><span class="bp">__MODULE__</span><span class="p" data-group-id="1307432362-9">{</span><span class="p" data-group-id="1307432362-9">}</span><span class="w">
    </span><span class="k" data-group-id="1307432362-6">end</span><span class="w">
  </span><span class="k" data-group-id="1307432362-4">end</span><span class="w">
</span><span class="k" data-group-id="1307432362-1">end</span></code></pre>

      ]]>
    </content>
  </entry>
  
  <entry>
    <title>IdempotencyPlug - idempotent POST requests</title>
    <link href="https://danschultzer.com/posts/idempotencyplug-idempotent-post-requests" />
    <id>https://danschultzer.com/posts/idempotencyplug-idempotent-post-requests</id>
    <updated>2023-04-07T00:00:00Z</updated>
    <summary>The motivations behind building IdempotencyPlug.</summary>
    <content type="html">
      <![CDATA[
        <p>
This week I released a plug library called <a href="https://hex.pm/packages/idempotency_plug"><code class="inline">IdempotencyPlug</code></a> to make POST (and PATCH) requests idempotent!</p>
<p>
While scouring the internet I didn’t find a whole lot of useful resources for this, no libraries and few articles. So in this blog post, I’ll detail what idempotent requests are, why you would need this, and how <code class="inline">IdempotencyPlug</code> works.</p>
<h3>
What is an idempotent request?</h3>
<p>
An idempotent request ensures that a request only affects the resource at most once. In REST all methods are idempotent except POST and PATCH.</p>
<p>
Take the endpoint <code class="inline">POST /api/payments</code> that initiate a new payment charge at a payment processor. If the client experiences a network interruption during the request, how can the client safely retry the request without creating a new charge?</p>
<p>
There are many ways this can be solved.</p>
<p>
A common approach, and the one I use for <code class="inline">IdempotencyPlug</code>, is to have the client send an <code class="inline">Idempotency-Key</code> HTTP header in the POST request. We know that we’re dealing with the same request acting on the same resource if the value of that header and the URI matches.</p>
<p>
If you want a deep dive, then I highly recommend reading Brandur’s <a href="https://brandur.org/idempotency-keys">blog post</a> on idempotency keys.</p>
<h3>
The requirements</h3>
<p>
I’m using the <a href="https://datatracker.ietf.org/doc/draft-ietf-httpapi-idempotency-key-header/">IETF Idempotency-Key Header draft</a> for all assumptions on how to deal with idempotency keys. These are the requirements for idempotent request handling:</p>
<ul>
  <li>
Require a single <code class="inline">Idempotency-Key</code> HTTP header for all POST and PATCH requests  </li>
  <li>
<code class="inline">Idempotency-Key</code> value MUST be unique for a URI  </li>
  <li>
<code class="inline">Idempotency-Key</code> value MUST NOT be reused with a different request payload  </li>
  <li>
First-time requests MUST be processed normally, and the response cached  </li>
  <li>
Duplicate requests MUST return the cached response  </li>
  <li>
Concurrent requests MUST return an error  </li>
  <li>
We MUST handle unexpected process termination  </li>
  <li>
The cached responses SHOULD expire after 24 hours  </li>
  <li>
The cache SHOULD be distributed and persisted  </li>
</ul>
<p>
I also had this special requirement for the specific environment I was dealing with:</p>
<ul>
  <li>
Handle globally isolated instances or intermittent isolations  </li>
</ul>
<p>
With this in hand, it’s time to build!</p>
<h3>
The GenServer</h3>
<p>
We’ll be using a GenServer since we must track unexpected process termination. The request process is monitored in the GenServer, and if the request response is never set we’ll update the cache when the process terminates.</p>
<p>
Here’s a snippet of how the logic works:</p>
<pre><code class="makeup elixir"><span class="kd">defmodule</span><span class="w"> </span><span class="nc">IdempotencyPlug.RequestTracker</span><span class="w"> </span><span class="k" data-group-id="1352314026-1">do</span><span class="w">
  </span><span class="kn">use</span><span class="w"> </span><span class="nc">GenServer</span><span class="w">

  </span><span class="c1">## API</span><span class="w">

  </span><span class="kd">def</span><span class="w"> </span><span class="nf">start_link</span><span class="p" data-group-id="1352314026-2">(</span><span class="n">opts</span><span class="p" data-group-id="1352314026-2">)</span><span class="w"> </span><span class="k" data-group-id="1352314026-3">do</span><span class="w">
    </span><span class="nc">GenServer</span><span class="o">.</span><span class="n">start_link</span><span class="p" data-group-id="1352314026-4">(</span><span class="bp">__MODULE__</span><span class="p">,</span><span class="w"> </span><span class="n">opts</span><span class="p" data-group-id="1352314026-4">)</span><span class="w">
  </span><span class="k" data-group-id="1352314026-3">end</span><span class="w">

  </span><span class="c1"># This must be called when we run the plug</span><span class="w">
  </span><span class="kd">def</span><span class="w"> </span><span class="nf">track</span><span class="p" data-group-id="1352314026-5">(</span><span class="n">name_or_pid</span><span class="p">,</span><span class="w"> </span><span class="n">request_id</span><span class="p">,</span><span class="w"> </span><span class="n">fingerprint</span><span class="p" data-group-id="1352314026-5">)</span><span class="w"> </span><span class="k" data-group-id="1352314026-6">do</span><span class="w">
    </span><span class="nc">GenServer</span><span class="o">.</span><span class="n">call</span><span class="p" data-group-id="1352314026-7">(</span><span class="n">name_or_pid</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="1352314026-8">{</span><span class="ss">:track</span><span class="p">,</span><span class="w"> </span><span class="n">request_id</span><span class="p">,</span><span class="w"> </span><span class="n">fingerprint</span><span class="p" data-group-id="1352314026-8">}</span><span class="p" data-group-id="1352314026-7">)</span><span class="w">
  </span><span class="k" data-group-id="1352314026-6">end</span><span class="w">

  </span><span class="c1"># This must be called in a before_send callback</span><span class="w">
  </span><span class="kd">def</span><span class="w"> </span><span class="nf">put_response</span><span class="p" data-group-id="1352314026-9">(</span><span class="n">name_or_pid</span><span class="p">,</span><span class="w"> </span><span class="n">request_id</span><span class="p">,</span><span class="w"> </span><span class="n">response</span><span class="p" data-group-id="1352314026-9">)</span><span class="w"> </span><span class="k" data-group-id="1352314026-10">do</span><span class="w">
    </span><span class="nc">GenServer</span><span class="o">.</span><span class="n">call</span><span class="p" data-group-id="1352314026-11">(</span><span class="n">name_or_pid</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="1352314026-12">{</span><span class="ss">:put_response</span><span class="p">,</span><span class="w"> </span><span class="n">request_id</span><span class="p">,</span><span class="w"> </span><span class="n">response</span><span class="p" data-group-id="1352314026-12">}</span><span class="p" data-group-id="1352314026-11">)</span><span class="w">
  </span><span class="k" data-group-id="1352314026-10">end</span><span class="w">

  </span><span class="c1">## Callbacks</span><span class="w">

  </span><span class="kd">def</span><span class="w"> </span><span class="nf">init</span><span class="p" data-group-id="1352314026-13">(</span><span class="c">_opts</span><span class="p" data-group-id="1352314026-13">)</span><span class="w"> </span><span class="k" data-group-id="1352314026-14">do</span><span class="w">
    </span><span class="p" data-group-id="1352314026-15">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="ss">monitored</span><span class="p">:</span><span class="w"> </span><span class="p" data-group-id="1352314026-16">[</span><span class="p" data-group-id="1352314026-16">]</span><span class="p" data-group-id="1352314026-15">}</span><span class="w">
  </span><span class="k" data-group-id="1352314026-14">end</span><span class="w">

  </span><span class="kd">def</span><span class="w"> </span><span class="nf">handle_call</span><span class="p" data-group-id="1352314026-17">(</span><span class="p" data-group-id="1352314026-18">{</span><span class="ss">:track</span><span class="p">,</span><span class="w"> </span><span class="n">request_id</span><span class="p">,</span><span class="w"> </span><span class="n">fingerprint</span><span class="p" data-group-id="1352314026-18">}</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="1352314026-19">{</span><span class="n">caller</span><span class="p">,</span><span class="w"> </span><span class="bp">_</span><span class="p" data-group-id="1352314026-19">}</span><span class="p">,</span><span class="w"> </span><span class="n">state</span><span class="p" data-group-id="1352314026-17">)</span><span class="w"> </span><span class="k" data-group-id="1352314026-20">do</span><span class="w">
    </span><span class="k">case</span><span class="w"> </span><span class="n">store</span><span class="o">.</span><span class="n">lookup</span><span class="p" data-group-id="1352314026-21">(</span><span class="n">request_id</span><span class="p" data-group-id="1352314026-21">)</span><span class="w"> </span><span class="k" data-group-id="1352314026-22">do</span><span class="w">
      </span><span class="ss">:not_found</span><span class="w"> </span><span class="o">-&gt;</span><span class="w">
        </span><span class="c1"># Store initial state in cache</span><span class="w">
        </span><span class="k">case</span><span class="w"> </span><span class="n">store</span><span class="o">.</span><span class="n">insert</span><span class="p" data-group-id="1352314026-23">(</span><span class="n">request_id</span><span class="p">,</span><span class="w"> </span><span class="ss">:processing</span><span class="p">,</span><span class="w"> </span><span class="n">fingerprint</span><span class="p">,</span><span class="w"> </span><span class="n">expires_at</span><span class="p" data-group-id="1352314026-23">)</span><span class="w"> </span><span class="k" data-group-id="1352314026-24">do</span><span class="w">
          </span><span class="ss">:ok</span><span class="w"> </span><span class="o">-&gt;</span><span class="w">
            </span><span class="c1"># Monitor request process</span><span class="w">
            </span><span class="n">monitored</span><span class="w"> </span><span class="o">=</span><span class="w">
              </span><span class="n">state</span><span class="o">.</span><span class="n">monitored</span><span class="w"> </span><span class="o">++</span><span class="w"> </span><span class="p" data-group-id="1352314026-25">[</span><span class="p" data-group-id="1352314026-26">{</span><span class="n">request_id</span><span class="p">,</span><span class="w"> </span><span class="n">caller</span><span class="p">,</span><span class="w"> </span><span class="nc">Process</span><span class="o">.</span><span class="n">monitor</span><span class="p" data-group-id="1352314026-27">(</span><span class="n">caller</span><span class="p" data-group-id="1352314026-27">)</span><span class="p" data-group-id="1352314026-26">}</span><span class="p" data-group-id="1352314026-25">]</span><span class="w">

            </span><span class="p" data-group-id="1352314026-28">{</span><span class="ss">:reply</span><span class="p">,</span><span class="w"> </span><span class="ss">:init</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="1352314026-29">%{</span><span class="n">state</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="ss">monitored</span><span class="p">:</span><span class="w"> </span><span class="n">monitored</span><span class="p" data-group-id="1352314026-29">}</span><span class="p" data-group-id="1352314026-28">}</span><span class="w">

          </span><span class="p" data-group-id="1352314026-30">{</span><span class="ss">:error</span><span class="p">,</span><span class="w"> </span><span class="n">reason</span><span class="p" data-group-id="1352314026-30">}</span><span class="w"> </span><span class="o">-&gt;</span><span class="w">
            </span><span class="p" data-group-id="1352314026-31">{</span><span class="ss">:reply</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="1352314026-32">{</span><span class="ss">:error</span><span class="p">,</span><span class="w"> </span><span class="n">reason</span><span class="p" data-group-id="1352314026-32">}</span><span class="p">,</span><span class="w"> </span><span class="n">state</span><span class="p" data-group-id="1352314026-31">}</span><span class="w">
        </span><span class="k" data-group-id="1352314026-24">end</span><span class="w">

      </span><span class="ss">:processing</span><span class="w"> </span><span class="o">-&gt;</span><span class="w">
        </span><span class="p" data-group-id="1352314026-33">{</span><span class="ss">:reply</span><span class="p">,</span><span class="w"> </span><span class="ss">:processing</span><span class="p">,</span><span class="w"> </span><span class="n">state</span><span class="p" data-group-id="1352314026-33">}</span><span class="w">

      </span><span class="p" data-group-id="1352314026-34">{</span><span class="ss">:halted</span><span class="p">,</span><span class="w"> </span><span class="n">reason</span><span class="p">,</span><span class="w"> </span><span class="o">^</span><span class="n">fingerprint</span><span class="p" data-group-id="1352314026-34">}</span><span class="w"> </span><span class="o">-&gt;</span><span class="w">
        </span><span class="p" data-group-id="1352314026-35">{</span><span class="ss">:reply</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="1352314026-36">{</span><span class="ss">:cache</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="1352314026-37">{</span><span class="ss">:halted</span><span class="p">,</span><span class="w"> </span><span class="n">reason</span><span class="p" data-group-id="1352314026-37">}</span><span class="p" data-group-id="1352314026-36">}</span><span class="p">,</span><span class="w"> </span><span class="n">state</span><span class="p" data-group-id="1352314026-35">}</span><span class="w">

      </span><span class="p" data-group-id="1352314026-38">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="n">response</span><span class="p">,</span><span class="w"> </span><span class="o">^</span><span class="n">fingerprint</span><span class="p" data-group-id="1352314026-38">}</span><span class="w"> </span><span class="o">-&gt;</span><span class="w">
        </span><span class="p" data-group-id="1352314026-39">{</span><span class="ss">:reply</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="1352314026-40">{</span><span class="ss">:cache</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="1352314026-41">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="n">response</span><span class="p" data-group-id="1352314026-41">}</span><span class="p" data-group-id="1352314026-40">}</span><span class="p">,</span><span class="w"> </span><span class="n">state</span><span class="p" data-group-id="1352314026-39">}</span><span class="w">

      </span><span class="c1"># When fingerprint is a mismatch for response and halted cache states</span><span class="w">
      </span><span class="p" data-group-id="1352314026-42">{</span><span class="c">_any</span><span class="p">,</span><span class="w"> </span><span class="c">_data</span><span class="p">,</span><span class="w"> </span><span class="c">_fingerprint</span><span class="p" data-group-id="1352314026-42">}</span><span class="w"> </span><span class="o">-&gt;</span><span class="w">
        </span><span class="p" data-group-id="1352314026-43">{</span><span class="ss">:reply</span><span class="p">,</span><span class="w"> </span><span class="ss">:invalid_fingerprint</span><span class="p">,</span><span class="w"> </span><span class="n">state</span><span class="p" data-group-id="1352314026-43">}</span><span class="w">
    </span><span class="k" data-group-id="1352314026-22">end</span><span class="w">
  </span><span class="k" data-group-id="1352314026-20">end</span><span class="w">

  </span><span class="kd">def</span><span class="w"> </span><span class="nf">handle_call</span><span class="p" data-group-id="1352314026-44">(</span><span class="p" data-group-id="1352314026-45">{</span><span class="ss">:put_response</span><span class="p">,</span><span class="w"> </span><span class="n">request_id</span><span class="p">,</span><span class="w"> </span><span class="n">response</span><span class="p" data-group-id="1352314026-45">}</span><span class="p">,</span><span class="w"> </span><span class="c">_from</span><span class="p">,</span><span class="w"> </span><span class="n">state</span><span class="p" data-group-id="1352314026-44">)</span><span class="w"> </span><span class="k" data-group-id="1352314026-46">do</span><span class="w">
    </span><span class="c1"># Fetch monitored requests from state by request_id</span><span class="w">
    </span><span class="p" data-group-id="1352314026-47">{</span><span class="n">unmonitored</span><span class="p">,</span><span class="w"> </span><span class="n">monitored</span><span class="p" data-group-id="1352314026-47">}</span><span class="w"> </span><span class="o">=</span><span class="w">
      </span><span class="nc">Enum</span><span class="o">.</span><span class="n">split_with</span><span class="p" data-group-id="1352314026-48">(</span><span class="n">state</span><span class="o">.</span><span class="n">monitored</span><span class="p">,</span><span class="w"> </span><span class="k" data-group-id="1352314026-49">fn</span><span class="w"> </span><span class="p" data-group-id="1352314026-50">{</span><span class="n">id</span><span class="p">,</span><span class="w"> </span><span class="bp">_</span><span class="p">,</span><span class="w"> </span><span class="bp">_</span><span class="p" data-group-id="1352314026-50">}</span><span class="w"> </span><span class="o">-&gt;</span><span class="w"> 
        </span><span class="n">id</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="n">request_id</span><span class="w">
      </span><span class="k" data-group-id="1352314026-49">end</span><span class="p" data-group-id="1352314026-48">)</span><span class="w">

    </span><span class="c1"># Demonitor request processes</span><span class="w">
    </span><span class="nc">Enum</span><span class="o">.</span><span class="n">each</span><span class="p" data-group-id="1352314026-51">(</span><span class="n">unmonitored</span><span class="p">,</span><span class="w"> </span><span class="k" data-group-id="1352314026-52">fn</span><span class="w"> </span><span class="p" data-group-id="1352314026-53">{</span><span class="bp">_</span><span class="p">,</span><span class="w"> </span><span class="bp">_</span><span class="p">,</span><span class="w"> </span><span class="n">ref</span><span class="p" data-group-id="1352314026-53">}</span><span class="w"> </span><span class="o">-&gt;</span><span class="w"> </span><span class="nc">Process</span><span class="o">.</span><span class="n">demonitor</span><span class="p" data-group-id="1352314026-54">(</span><span class="n">ref</span><span class="p" data-group-id="1352314026-54">)</span><span class="w"> </span><span class="k" data-group-id="1352314026-52">end</span><span class="p" data-group-id="1352314026-51">)</span><span class="w">
    </span><span class="n">state</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p" data-group-id="1352314026-55">%{</span><span class="n">state</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="ss">monitored</span><span class="p">:</span><span class="w"> </span><span class="n">monitored</span><span class="p" data-group-id="1352314026-55">}</span><span class="w">

    </span><span class="c1"># Update with response in cache</span><span class="w">
    </span><span class="k">case</span><span class="w"> </span><span class="n">store</span><span class="o">.</span><span class="n">update</span><span class="p" data-group-id="1352314026-56">(</span><span class="n">request_id</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="1352314026-57">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="n">response</span><span class="p" data-group-id="1352314026-57">}</span><span class="p">,</span><span class="w"> </span><span class="n">expires_at</span><span class="p" data-group-id="1352314026-56">)</span><span class="w"> </span><span class="k" data-group-id="1352314026-58">do</span><span class="w">
      </span><span class="ss">:ok</span><span class="w"> </span><span class="o">-&gt;</span><span class="w"> </span><span class="p" data-group-id="1352314026-59">{</span><span class="ss">:reply</span><span class="p">,</span><span class="w"> </span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="n">state</span><span class="p" data-group-id="1352314026-59">}</span><span class="w">
      </span><span class="p" data-group-id="1352314026-60">{</span><span class="ss">:error</span><span class="p">,</span><span class="w"> </span><span class="n">reason</span><span class="p" data-group-id="1352314026-60">}</span><span class="w"> </span><span class="o">-&gt;</span><span class="w"> </span><span class="p" data-group-id="1352314026-61">{</span><span class="ss">:reply</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="1352314026-62">{</span><span class="ss">:error</span><span class="p">,</span><span class="w"> </span><span class="n">reason</span><span class="p" data-group-id="1352314026-62">}</span><span class="p">,</span><span class="w"> </span><span class="n">state</span><span class="p" data-group-id="1352314026-61">}</span><span class="w">
    </span><span class="k" data-group-id="1352314026-58">end</span><span class="w">
  </span><span class="k" data-group-id="1352314026-46">end</span><span class="w">

  </span><span class="kd">def</span><span class="w"> </span><span class="nf">handle_info</span><span class="p" data-group-id="1352314026-63">(</span><span class="p" data-group-id="1352314026-64">{</span><span class="ss">:DOWN</span><span class="p">,</span><span class="w"> </span><span class="c">_ref</span><span class="p">,</span><span class="w"> </span><span class="ss">:process</span><span class="p">,</span><span class="w"> </span><span class="n">pid</span><span class="p">,</span><span class="w"> </span><span class="n">reason</span><span class="p" data-group-id="1352314026-64">}</span><span class="p">,</span><span class="w"> </span><span class="n">state</span><span class="p" data-group-id="1352314026-63">)</span><span class="w"> </span><span class="k" data-group-id="1352314026-65">do</span><span class="w">
    </span><span class="c1"># Fetch monitored requests from state by pid</span><span class="w">
    </span><span class="p" data-group-id="1352314026-66">{</span><span class="n">terminated</span><span class="p">,</span><span class="w"> </span><span class="n">monitored</span><span class="p" data-group-id="1352314026-66">}</span><span class="w"> </span><span class="o">=</span><span class="w">
      </span><span class="nc">Enum</span><span class="o">.</span><span class="n">split_with</span><span class="p" data-group-id="1352314026-67">(</span><span class="n">state</span><span class="o">.</span><span class="n">monitored</span><span class="p">,</span><span class="w"> </span><span class="k" data-group-id="1352314026-68">fn</span><span class="w"> </span><span class="p" data-group-id="1352314026-69">{</span><span class="bp">_</span><span class="p">,</span><span class="w"> </span><span class="n">caller</span><span class="p">,</span><span class="w"> </span><span class="bp">_</span><span class="p" data-group-id="1352314026-69">}</span><span class="w"> </span><span class="o">-&gt;</span><span class="w">
        </span><span class="n">caller</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="n">pid</span><span class="w">
      </span><span class="k" data-group-id="1352314026-68">end</span><span class="p" data-group-id="1352314026-67">)</span><span class="w">

    </span><span class="c1"># Demonitor request processes</span><span class="w">
    </span><span class="nc">Enum</span><span class="o">.</span><span class="n">each</span><span class="p" data-group-id="1352314026-70">(</span><span class="n">terminated</span><span class="p">,</span><span class="w"> </span><span class="k" data-group-id="1352314026-71">fn</span><span class="w"> </span><span class="p" data-group-id="1352314026-72">{</span><span class="bp">_</span><span class="p">,</span><span class="w"> </span><span class="bp">_</span><span class="p">,</span><span class="w"> </span><span class="n">ref</span><span class="p" data-group-id="1352314026-72">}</span><span class="w"> </span><span class="o">-&gt;</span><span class="w"> </span><span class="nc">Process</span><span class="o">.</span><span class="n">demonitor</span><span class="p" data-group-id="1352314026-73">(</span><span class="n">ref</span><span class="p" data-group-id="1352314026-73">)</span><span class="w"> </span><span class="k" data-group-id="1352314026-71">end</span><span class="p" data-group-id="1352314026-70">)</span><span class="w">
    </span><span class="n">state</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p" data-group-id="1352314026-74">%{</span><span class="n">state</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="ss">monitored</span><span class="p">:</span><span class="w"> </span><span class="n">monitored</span><span class="p" data-group-id="1352314026-74">}</span><span class="w">

    </span><span class="c1"># Update with halted state in cache</span><span class="w">
    </span><span class="nc">Enum</span><span class="o">.</span><span class="n">each</span><span class="p" data-group-id="1352314026-75">(</span><span class="n">terminated</span><span class="p">,</span><span class="w"> </span><span class="k" data-group-id="1352314026-76">fn</span><span class="w"> </span><span class="p" data-group-id="1352314026-77">{</span><span class="n">request_id</span><span class="p">,</span><span class="w"> </span><span class="bp">_</span><span class="p">,</span><span class="w"> </span><span class="bp">_</span><span class="p" data-group-id="1352314026-77">}</span><span class="w"> </span><span class="o">-&gt;</span><span class="w">
      </span><span class="n">store</span><span class="o">.</span><span class="n">update</span><span class="p" data-group-id="1352314026-78">(</span><span class="n">request_id</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="1352314026-79">{</span><span class="ss">:halted</span><span class="p">,</span><span class="w"> </span><span class="n">reason</span><span class="p" data-group-id="1352314026-79">}</span><span class="p">,</span><span class="w"> </span><span class="n">expires_at</span><span class="p" data-group-id="1352314026-78">)</span><span class="w">
    </span><span class="k" data-group-id="1352314026-76">end</span><span class="p" data-group-id="1352314026-75">)</span><span class="w">

    </span><span class="p" data-group-id="1352314026-80">{</span><span class="ss">:noreply</span><span class="p">,</span><span class="w"> </span><span class="n">state</span><span class="p" data-group-id="1352314026-80">}</span><span class="w">
  </span><span class="k" data-group-id="1352314026-65">end</span><span class="w">
</span><span class="k" data-group-id="1352314026-1">end</span></code></pre>
<h3>
The Plug</h3>
<p>
The plug will parse the <code class="inline">Idempotency-Key</code> HTTP header, store responses for first-time requests, and return cached responses for subsequent requests.</p>
<pre><code class="makeup elixir"><span class="kd">defmodule</span><span class="w"> </span><span class="nc">IdempotencyPlug</span><span class="w"> </span><span class="k" data-group-id="5769087225-1">do</span><span class="w">
  </span><span class="kn">alias</span><span class="w"> </span><span class="nc">IdempotencyPlug.RequestTracker</span><span class="w">

  </span><span class="kd">def</span><span class="w"> </span><span class="nf">init</span><span class="p" data-group-id="5769087225-2">(</span><span class="n">opts</span><span class="p" data-group-id="5769087225-2">)</span><span class="p">,</span><span class="w"> </span><span class="ss">do</span><span class="p">:</span><span class="w"> </span><span class="n">opts</span><span class="w">

  </span><span class="kd">def</span><span class="w"> </span><span class="nf">call</span><span class="p" data-group-id="5769087225-3">(</span><span class="p" data-group-id="5769087225-4">%{</span><span class="ss">method</span><span class="p">:</span><span class="w"> </span><span class="n">method</span><span class="p" data-group-id="5769087225-4">}</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">conn</span><span class="p">,</span><span class="w"> </span><span class="n">opts</span><span class="p" data-group-id="5769087225-3">)</span><span class="w"> </span><span class="ow">when</span><span class="w"> </span><span class="n">method</span><span class="w"> </span><span class="ow">in</span><span class="w"> </span><span class="sx">~w(POST PATCH)</span><span class="w"> </span><span class="k" data-group-id="5769087225-5">do</span><span class="w">
    </span><span class="k">case</span><span class="w"> </span><span class="nc">Plug.Conn</span><span class="o">.</span><span class="n">get_req_header</span><span class="p" data-group-id="5769087225-6">(</span><span class="n">conn</span><span class="p">,</span><span class="w"> </span><span class="s">&quot;idempotency-key&quot;</span><span class="p" data-group-id="5769087225-6">)</span><span class="w"> </span><span class="k" data-group-id="5769087225-7">do</span><span class="w">
      </span><span class="p" data-group-id="5769087225-8">[</span><span class="n">id</span><span class="p" data-group-id="5769087225-8">]</span><span class="w"> </span><span class="o">-&gt;</span><span class="w"> </span><span class="n">handle_idempotent_request</span><span class="p" data-group-id="5769087225-9">(</span><span class="n">conn</span><span class="p">,</span><span class="w"> </span><span class="n">id</span><span class="p">,</span><span class="w"> </span><span class="n">opts</span><span class="p" data-group-id="5769087225-9">)</span><span class="w">
      </span><span class="p" data-group-id="5769087225-10">[</span><span class="bp">_</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="bp">_</span><span class="p" data-group-id="5769087225-10">]</span><span class="w"> </span><span class="o">-&gt;</span><span class="w"> </span><span class="c1"># Raise or return error</span><span class="w">
      </span><span class="p" data-group-id="5769087225-11">[</span><span class="p" data-group-id="5769087225-11">]</span><span class="w"> </span><span class="o">-&gt;</span><span class="w"> </span><span class="c1"># Raise or return error</span><span class="w">
    </span><span class="k" data-group-id="5769087225-7">end</span><span class="w">
  </span><span class="k" data-group-id="5769087225-5">end</span><span class="w">

  </span><span class="kd">def</span><span class="w"> </span><span class="nf">call</span><span class="p" data-group-id="5769087225-12">(</span><span class="n">conn</span><span class="p">,</span><span class="w"> </span><span class="c">_opts</span><span class="p" data-group-id="5769087225-12">)</span><span class="p">,</span><span class="w"> </span><span class="ss">do</span><span class="p">:</span><span class="w"> </span><span class="n">conn</span><span class="w">

  </span><span class="kd">defp</span><span class="w"> </span><span class="nf">handle_idempotent_request</span><span class="p" data-group-id="5769087225-13">(</span><span class="n">conn</span><span class="p">,</span><span class="w"> </span><span class="n">id</span><span class="p">,</span><span class="w"> </span><span class="n">opts</span><span class="p" data-group-id="5769087225-13">)</span><span class="w"> </span><span class="k" data-group-id="5769087225-14">do</span><span class="w">
    </span><span class="n">idempotent_id</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">sha256_hash</span><span class="p" data-group-id="5769087225-15">(</span><span class="p" data-group-id="5769087225-16">{</span><span class="n">id</span><span class="p">,</span><span class="w"> </span><span class="n">conn</span><span class="o">.</span><span class="n">path_info</span><span class="p" data-group-id="5769087225-16">}</span><span class="p" data-group-id="5769087225-15">)</span><span class="w">
    </span><span class="n">fingerprint</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">sha256_hash</span><span class="p" data-group-id="5769087225-17">(</span><span class="n">conn</span><span class="o">.</span><span class="n">params</span><span class="w"> </span><span class="o">|&gt;</span><span class="w"> </span><span class="nc">Map</span><span class="o">.</span><span class="n">to_list</span><span class="p" data-group-id="5769087225-18">(</span><span class="p" data-group-id="5769087225-18">)</span><span class="w"> </span><span class="o">|&gt;</span><span class="w"> </span><span class="nc">Enum</span><span class="o">.</span><span class="n">sort</span><span class="p" data-group-id="5769087225-19">(</span><span class="p" data-group-id="5769087225-19">)</span><span class="p" data-group-id="5769087225-17">)</span><span class="w">

    </span><span class="k">case</span><span class="w"> </span><span class="nc">RequestTracker</span><span class="o">.</span><span class="n">track</span><span class="p" data-group-id="5769087225-20">(</span><span class="n">tracker</span><span class="p">,</span><span class="w"> </span><span class="n">idempotent_id</span><span class="p">,</span><span class="w"> </span><span class="n">fingerprint</span><span class="p" data-group-id="5769087225-20">)</span><span class="w"> </span><span class="k" data-group-id="5769087225-21">do</span><span class="w">
      </span><span class="ss">:processing</span><span class="w"> </span><span class="o">-&gt;</span><span class="w">
        </span><span class="c1"># Raise or return concurrent request error</span><span class="w">

      </span><span class="ss">:invalid_fingerprint</span><span class="w"> </span><span class="o">-&gt;</span><span class="w">
        </span><span class="c1"># Raise or return fingerprint mismatch error</span><span class="w">

      </span><span class="p" data-group-id="5769087225-22">{</span><span class="ss">:cache</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="5769087225-23">{</span><span class="ss">:halted</span><span class="p">,</span><span class="w"> </span><span class="c">_reason</span><span class="p" data-group-id="5769087225-23">}</span><span class="p" data-group-id="5769087225-22">}</span><span class="w"> </span><span class="o">-&gt;</span><span class="w">
        </span><span class="c1"># Raise or return unexpected process termination error</span><span class="w">

      </span><span class="p" data-group-id="5769087225-24">{</span><span class="ss">:cache</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="5769087225-25">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="5769087225-26">%{</span><span class="ss">resp_body</span><span class="p">:</span><span class="w"> </span><span class="n">body</span><span class="p">,</span><span class="w"> </span><span class="ss">resp_headers</span><span class="p">:</span><span class="w"> </span><span class="n">headers</span><span class="p">,</span><span class="w"> </span><span class="ss">status</span><span class="p">:</span><span class="w"> </span><span class="n">status</span><span class="p" data-group-id="5769087225-26">}</span><span class="p" data-group-id="5769087225-25">}</span><span class="p" data-group-id="5769087225-24">}</span><span class="w"> </span><span class="o">-&gt;</span><span class="w">
        </span><span class="n">headers</span><span class="w">
        </span><span class="o">|&gt;</span><span class="w"> </span><span class="nc">Enum</span><span class="o">.</span><span class="n">reduce</span><span class="p" data-group-id="5769087225-27">(</span><span class="n">conn</span><span class="p">,</span><span class="w"> </span><span class="k" data-group-id="5769087225-28">fn</span><span class="w"> </span><span class="p" data-group-id="5769087225-29">{</span><span class="n">key</span><span class="p">,</span><span class="w"> </span><span class="n">value</span><span class="p" data-group-id="5769087225-29">}</span><span class="p">,</span><span class="w"> </span><span class="n">conn</span><span class="w"> </span><span class="o">-&gt;</span><span class="w">
          </span><span class="nc">Plug.Conn</span><span class="o">.</span><span class="n">put_resp_header</span><span class="p" data-group-id="5769087225-30">(</span><span class="n">conn</span><span class="p">,</span><span class="w"> </span><span class="n">key</span><span class="p">,</span><span class="w"> </span><span class="n">value</span><span class="p" data-group-id="5769087225-30">)</span><span class="w">
        </span><span class="k" data-group-id="5769087225-28">end</span><span class="p" data-group-id="5769087225-27">)</span><span class="w">
        </span><span class="o">|&gt;</span><span class="w"> </span><span class="nc">Plug.Conn</span><span class="o">.</span><span class="n">resp</span><span class="p" data-group-id="5769087225-31">(</span><span class="n">status</span><span class="p">,</span><span class="w"> </span><span class="n">body</span><span class="p" data-group-id="5769087225-31">)</span><span class="w">
        </span><span class="o">|&gt;</span><span class="w"> </span><span class="nc">Plug.Conn</span><span class="o">.</span><span class="n">halt</span><span class="p" data-group-id="5769087225-32">(</span><span class="p" data-group-id="5769087225-32">)</span><span class="w">

      </span><span class="ss">:init</span><span class="w"> </span><span class="o">-&gt;</span><span class="w">
        </span><span class="nc">Plug.Conn</span><span class="o">.</span><span class="n">register_before_send</span><span class="p" data-group-id="5769087225-33">(</span><span class="n">conn</span><span class="p">,</span><span class="w"> </span><span class="k" data-group-id="5769087225-34">fn</span><span class="w"> </span><span class="n">conn</span><span class="w"> </span><span class="o">-&gt;</span><span class="w">
          </span><span class="n">data</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">Map</span><span class="o">.</span><span class="n">take</span><span class="p" data-group-id="5769087225-35">(</span><span class="n">conn</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="5769087225-36">[</span><span class="ss">:resp_body</span><span class="p">,</span><span class="w"> </span><span class="ss">:resp_headers</span><span class="p">,</span><span class="w"> </span><span class="ss">:status</span><span class="p" data-group-id="5769087225-36">]</span><span class="p" data-group-id="5769087225-35">)</span><span class="w">

          </span><span class="k">case</span><span class="w"> </span><span class="nc">RequestTracker</span><span class="o">.</span><span class="n">put_response</span><span class="p" data-group-id="5769087225-37">(</span><span class="n">tracker</span><span class="p">,</span><span class="w"> </span><span class="n">idempotent_id</span><span class="p">,</span><span class="w"> </span><span class="n">data</span><span class="p" data-group-id="5769087225-37">)</span><span class="w"> </span><span class="k" data-group-id="5769087225-38">do</span><span class="w">
            </span><span class="ss">:ok</span><span class="w"> </span><span class="o">-&gt;</span><span class="w"> </span><span class="n">conn</span><span class="w">
            </span><span class="p" data-group-id="5769087225-39">{</span><span class="ss">:error</span><span class="p">,</span><span class="w"> </span><span class="n">error</span><span class="p" data-group-id="5769087225-39">}</span><span class="w"> </span><span class="o">-&gt;</span><span class="w"> </span><span class="c1"># Raise error</span><span class="w">
          </span><span class="k" data-group-id="5769087225-38">end</span><span class="w">
        </span><span class="k" data-group-id="5769087225-34">end</span><span class="p" data-group-id="5769087225-33">)</span><span class="w">

      </span><span class="p" data-group-id="5769087225-40">{</span><span class="ss">:error</span><span class="p">,</span><span class="w"> </span><span class="n">error</span><span class="p" data-group-id="5769087225-40">}</span><span class="w"> </span><span class="o">-&gt;</span><span class="w">
        </span><span class="c1"># Raise error</span><span class="w">
    </span><span class="k" data-group-id="5769087225-21">end</span><span class="w">
  </span><span class="k" data-group-id="5769087225-14">end</span><span class="w">
</span><span class="k" data-group-id="5769087225-1">end</span></code></pre>
<h3>
Distribution and persistence</h3>
<p>
An ETS store is used as the default store in <code class="inline">IdempotencyPlug</code>. It requires the least configuration to get going, but it’s not distributed and persisted. In my specific use case, I couldn’t depend on cluster nodes to always be connected. So I’m using the Ecto store that’s shipped with <code class="inline">IdempotencyPlug</code> for persisted and distributed caching.</p>
<p>
There are many more ways distribution and persistence can be dealt with instead, for example:</p>
<ul>
  <li>
Use <a href="https://hexdocs.pm/phoenix_pubsub/Phoenix.PubSub.html"><code class="inline">Phoenix.PubSub</code></a> for distribution  </li>
  <li>
Use <a href="https://www.erlang.org/doc/man/dets.html">disk-based ETS</a> for persistence  </li>
  <li>
Use a distributed cache library like <a href="https://github.com/whitfin/cachex"><code class="inline">Cachex</code></a>  </li>
</ul>
<p>
To that end <code class="inline">IdempotencyPlug</code> ships with an <a href="https://hexdocs.pm/idempotency_plug/IdempotencyPlug.Store.html"><code class="inline">IdempotencyPlug.Store</code></a> behaviour for any store implementations.</p>
<h3>
Testing with Phoenix</h3>
<p>
After implementing <code class="inline">IdempotencyPlug</code> you may have some custom configuration you want to ensure works. In my case, I had to ensure that the responses were correct since I use <a href="https://hex.pm/packages/open_api_spex"><code class="inline">OpenAPISpex</code></a>, and also ensure that the idempotent request was scoped to the authenticated user. The custom <code class="inline">:with</code> error handler returned a halted conn with a JSON response instead of raising the error.</p>
<p>
I implemented a test controller that only was included for tests:</p>
<pre><code class="makeup elixir"><span class="k">if</span><span class="w"> </span><span class="nc">Mix</span><span class="o">.</span><span class="n">env</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="ss">:test</span><span class="w"> </span><span class="k" data-group-id="1949303055-1">do</span><span class="w">
  </span><span class="n">post</span><span class="w"> </span><span class="s">&quot;/idempotency-plug-handler-test&quot;</span><span class="p">,</span><span class="w"> </span><span class="nc">IdempotencyPlugHandlerTestController</span><span class="p">,</span><span class="w"> </span><span class="ss">:create</span><span class="w">
</span><span class="k" data-group-id="1949303055-1">end</span></code></pre>
<pre><code class="makeup elixir"><span class="c1"># test/support/idempotency_plug_handler_test_controller.ex</span><span class="w">
</span><span class="kd">defmodule</span><span class="w"> </span><span class="nc">MyAppWeb.IdempotencyPlugHandlerTestController</span><span class="w"> </span><span class="k" data-group-id="5610265182-1">do</span><span class="w">
  </span><span class="kn">use</span><span class="w"> </span><span class="nc">MyAppWeb</span><span class="p">,</span><span class="w"> </span><span class="ss">:controller</span><span class="w">

  </span><span class="kd">def</span><span class="w"> </span><span class="nf">create</span><span class="p" data-group-id="5610265182-2">(</span><span class="n">conn</span><span class="p">,</span><span class="w"> </span><span class="c">_params</span><span class="p" data-group-id="5610265182-2">)</span><span class="w"> </span><span class="k" data-group-id="5610265182-3">do</span><span class="w">
    </span><span class="n">message</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">if</span><span class="w"> </span><span class="n">callback</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">Process</span><span class="o">.</span><span class="n">get</span><span class="p" data-group-id="5610265182-4">(</span><span class="ss">:callback</span><span class="p" data-group-id="5610265182-4">)</span><span class="p">,</span><span class="w"> </span><span class="ss">do</span><span class="p">:</span><span class="w"> </span><span class="n">callback</span><span class="o">.</span><span class="p" data-group-id="5610265182-5">(</span><span class="p" data-group-id="5610265182-5">)</span><span class="p">,</span><span class="w"> </span><span class="ss">else</span><span class="p">:</span><span class="w"> </span><span class="s">&quot;OK&quot;</span><span class="w">

    </span><span class="n">json</span><span class="p" data-group-id="5610265182-6">(</span><span class="n">conn</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="5610265182-7">%{</span><span class="ss">message</span><span class="p">:</span><span class="w"> </span><span class="n">message</span><span class="p" data-group-id="5610265182-7">}</span><span class="p" data-group-id="5610265182-6">)</span><span class="w">
  </span><span class="k" data-group-id="5610265182-3">end</span><span class="w">
</span><span class="k" data-group-id="5610265182-1">end</span></code></pre>
<p>
And then ran through the whole pipeline in tests:</p>
<pre><code class="makeup elixir"><span class="kd">defmodule</span><span class="w"> </span><span class="nc">MyAppWeb.IdempotencyPlugHandlerTest</span><span class="w"> </span><span class="k" data-group-id="2312876431-1">do</span><span class="w">
  </span><span class="kn">use</span><span class="w"> </span><span class="nc">MyAppWeb.ConnCase</span><span class="w">

  </span><span class="n">setup</span><span class="w"> </span><span class="p" data-group-id="2312876431-2">[</span><span class="ss">:setup_authenticated_conn</span><span class="p">,</span><span class="w"> </span><span class="ss">:setup_request</span><span class="p" data-group-id="2312876431-2">]</span><span class="w">

  </span><span class="n">test</span><span class="w"> </span><span class="s">&quot;with concurrent request&quot;</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="2312876431-3">%{</span><span class="ss">conn</span><span class="p">:</span><span class="w"> </span><span class="n">conn</span><span class="p" data-group-id="2312876431-3">}</span><span class="w"> </span><span class="k" data-group-id="2312876431-4">do</span><span class="w">
    </span><span class="n">pid</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">self</span><span class="p" data-group-id="2312876431-5">(</span><span class="p" data-group-id="2312876431-5">)</span><span class="w">

    </span><span class="n">task</span><span class="w"> </span><span class="o">=</span><span class="w">
      </span><span class="nc">Task</span><span class="o">.</span><span class="n">async</span><span class="p" data-group-id="2312876431-6">(</span><span class="k" data-group-id="2312876431-7">fn</span><span class="w"> </span><span class="o">-&gt;</span><span class="w">
        </span><span class="n">post_request</span><span class="p" data-group-id="2312876431-8">(</span><span class="n">conn</span><span class="p">,</span><span class="w"> </span><span class="k" data-group-id="2312876431-9">fn</span><span class="w"> </span><span class="o">-&gt;</span><span class="w">
          </span><span class="n">send</span><span class="p" data-group-id="2312876431-10">(</span><span class="n">pid</span><span class="p">,</span><span class="w"> </span><span class="ss">:continue</span><span class="p" data-group-id="2312876431-10">)</span><span class="w">
          </span><span class="k">receive</span><span class="w"> </span><span class="k" data-group-id="2312876431-11">do</span><span class="w">
            </span><span class="ss">:continue</span><span class="w"> </span><span class="o">-&gt;</span><span class="w"> </span><span class="ss">:ok</span><span class="w">
          </span><span class="k" data-group-id="2312876431-11">end</span><span class="w">

          </span><span class="ss">:ok</span><span class="w">
        </span><span class="k" data-group-id="2312876431-9">end</span><span class="p" data-group-id="2312876431-8">)</span><span class="w">
      </span><span class="k" data-group-id="2312876431-7">end</span><span class="p" data-group-id="2312876431-6">)</span><span class="w">

    </span><span class="k">receive</span><span class="w"> </span><span class="k" data-group-id="2312876431-12">do</span><span class="w">
      </span><span class="ss">:continue</span><span class="w"> </span><span class="o">-&gt;</span><span class="w"> </span><span class="ss">:ok</span><span class="w">
    </span><span class="k" data-group-id="2312876431-12">end</span><span class="w">

    </span><span class="n">conn</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">post_request</span><span class="p" data-group-id="2312876431-13">(</span><span class="n">conn</span><span class="p" data-group-id="2312876431-13">)</span><span class="w">

    </span><span class="n">assert</span><span class="w"> </span><span class="n">conn</span><span class="o">.</span><span class="n">halted</span><span class="w">

    </span><span class="c1"># assert custom json response</span><span class="w">

    </span><span class="n">send</span><span class="p" data-group-id="2312876431-14">(</span><span class="n">task</span><span class="o">.</span><span class="n">pid</span><span class="p">,</span><span class="w"> </span><span class="ss">:continue</span><span class="p" data-group-id="2312876431-14">)</span><span class="w">
    </span><span class="nc">Task</span><span class="o">.</span><span class="n">await</span><span class="p" data-group-id="2312876431-15">(</span><span class="n">task</span><span class="p" data-group-id="2312876431-15">)</span><span class="w">
  </span><span class="k" data-group-id="2312876431-4">end</span><span class="w">

  </span><span class="na">@tag</span><span class="w"> </span><span class="ss">capture_log</span><span class="p">:</span><span class="w"> </span><span class="no">true</span><span class="w">
  </span><span class="n">test</span><span class="w"> </span><span class="s">&quot;with halted response&quot;</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="2312876431-16">%{</span><span class="ss">conn</span><span class="p">:</span><span class="w"> </span><span class="n">conn</span><span class="p" data-group-id="2312876431-16">}</span><span class="w"> </span><span class="k" data-group-id="2312876431-17">do</span><span class="w">
    </span><span class="nc">Process</span><span class="o">.</span><span class="n">flag</span><span class="p" data-group-id="2312876431-18">(</span><span class="ss">:trap_exit</span><span class="p">,</span><span class="w"> </span><span class="no">true</span><span class="p" data-group-id="2312876431-18">)</span><span class="w">
    </span><span class="n">task</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">Task</span><span class="o">.</span><span class="n">async</span><span class="p" data-group-id="2312876431-19">(</span><span class="k" data-group-id="2312876431-20">fn</span><span class="w"> </span><span class="o">-&gt;</span><span class="w"> </span><span class="n">post_request</span><span class="p" data-group-id="2312876431-21">(</span><span class="n">conn</span><span class="p">,</span><span class="w"> </span><span class="k" data-group-id="2312876431-22">fn</span><span class="w"> </span><span class="o">-&gt;</span><span class="w"> </span><span class="n">exit</span><span class="p" data-group-id="2312876431-23">(</span><span class="ss">:fatal</span><span class="p" data-group-id="2312876431-23">)</span><span class="w"> </span><span class="k" data-group-id="2312876431-22">end</span><span class="p" data-group-id="2312876431-21">)</span><span class="w"> </span><span class="k" data-group-id="2312876431-20">end</span><span class="p" data-group-id="2312876431-19">)</span><span class="w">
    </span><span class="p" data-group-id="2312876431-24">{</span><span class="ss">:fatal</span><span class="p">,</span><span class="w"> </span><span class="bp">_</span><span class="p" data-group-id="2312876431-24">}</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">catch_exit</span><span class="p" data-group-id="2312876431-25">(</span><span class="nc">Task</span><span class="o">.</span><span class="n">await</span><span class="p" data-group-id="2312876431-26">(</span><span class="n">task</span><span class="p" data-group-id="2312876431-26">)</span><span class="p" data-group-id="2312876431-25">)</span><span class="w">

    </span><span class="n">conn</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">post_request</span><span class="p" data-group-id="2312876431-27">(</span><span class="n">conn</span><span class="p" data-group-id="2312876431-27">)</span><span class="w">

    </span><span class="n">assert</span><span class="w"> </span><span class="n">conn</span><span class="o">.</span><span class="n">halted</span><span class="w">

    </span><span class="c1"># assert custom json response</span><span class="w">
  </span><span class="k" data-group-id="2312876431-17">end</span><span class="w">

  </span><span class="n">test</span><span class="w"> </span><span class="s">&quot;with cached response with different request payload&quot;</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="2312876431-28">%{</span><span class="ss">conn</span><span class="p">:</span><span class="w"> </span><span class="n">conn</span><span class="p" data-group-id="2312876431-28">}</span><span class="w"> </span><span class="k" data-group-id="2312876431-29">do</span><span class="w">
    </span><span class="c">_other_conn</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">post_request</span><span class="p" data-group-id="2312876431-30">(</span><span class="n">conn</span><span class="p">,</span><span class="w"> </span><span class="no">nil</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="2312876431-31">%{</span><span class="s">&quot;other-key&quot;</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="s">&quot;1&quot;</span><span class="p" data-group-id="2312876431-31">}</span><span class="p" data-group-id="2312876431-30">)</span><span class="w">
    </span><span class="n">conn</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">post_request</span><span class="p" data-group-id="2312876431-32">(</span><span class="n">conn</span><span class="p" data-group-id="2312876431-32">)</span><span class="w">

    </span><span class="n">assert</span><span class="w"> </span><span class="n">conn</span><span class="o">.</span><span class="n">halted</span><span class="w">

    </span><span class="c1"># assert custom json response</span><span class="w">
  </span><span class="k" data-group-id="2312876431-29">end</span><span class="w">

  </span><span class="n">test</span><span class="w"> </span><span class="s">&quot;with cached response with different user&quot;</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="2312876431-33">%{</span><span class="ss">conn</span><span class="p">:</span><span class="w"> </span><span class="n">conn</span><span class="p" data-group-id="2312876431-33">}</span><span class="w"> </span><span class="k" data-group-id="2312876431-34">do</span><span class="w">
    </span><span class="c1"># Set up authentication with different user for other_conn</span><span class="w">
    </span><span class="p" data-group-id="2312876431-35">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="n">context</span><span class="p" data-group-id="2312876431-35">}</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">setup_authenticated_conn</span><span class="p" data-group-id="2312876431-36">(</span><span class="p" data-group-id="2312876431-37">%{</span><span class="ss">conn</span><span class="p">:</span><span class="w"> </span><span class="n">conn</span><span class="p" data-group-id="2312876431-37">}</span><span class="p" data-group-id="2312876431-36">)</span><span class="w">
    </span><span class="n">other_conn</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">post_request</span><span class="p" data-group-id="2312876431-38">(</span><span class="n">context</span><span class="p" data-group-id="2312876431-39">[</span><span class="ss">:conn</span><span class="p" data-group-id="2312876431-39">]</span><span class="p">,</span><span class="w"> </span><span class="k" data-group-id="2312876431-40">fn</span><span class="w"> </span><span class="o">-&gt;</span><span class="w"> </span><span class="s">&quot;OTHER-RESPONSE&quot;</span><span class="w"> </span><span class="k" data-group-id="2312876431-40">end</span><span class="p" data-group-id="2312876431-38">)</span><span class="w">

    </span><span class="n">conn</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">post_request</span><span class="p" data-group-id="2312876431-41">(</span><span class="n">conn</span><span class="p" data-group-id="2312876431-41">)</span><span class="w">

    </span><span class="n">refute</span><span class="w"> </span><span class="n">conn</span><span class="o">.</span><span class="n">halted</span><span class="w">
    </span><span class="n">refute</span><span class="w"> </span><span class="n">conn</span><span class="o">.</span><span class="n">resp_body</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="n">other_conn</span><span class="o">.</span><span class="n">resp_body</span><span class="w">
  </span><span class="k" data-group-id="2312876431-34">end</span><span class="w">

  </span><span class="kd">defp</span><span class="w"> </span><span class="nf">setup_request</span><span class="p" data-group-id="2312876431-42">(</span><span class="p" data-group-id="2312876431-43">%{</span><span class="ss">conn</span><span class="p">:</span><span class="w"> </span><span class="n">conn</span><span class="p" data-group-id="2312876431-43">}</span><span class="p" data-group-id="2312876431-42">)</span><span class="w"> </span><span class="k" data-group-id="2312876431-44">do</span><span class="w">
    </span><span class="n">conn</span><span class="w"> </span><span class="o">=</span><span class="w">
      </span><span class="n">conn</span><span class="w">
      </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">put_req_header</span><span class="p" data-group-id="2312876431-45">(</span><span class="s">&quot;idempotency-key&quot;</span><span class="p">,</span><span class="w"> </span><span class="s">&quot;key&quot;</span><span class="p" data-group-id="2312876431-45">)</span><span class="w">
      </span><span class="o">|&gt;</span><span class="w"> </span><span class="nc">Map</span><span class="o">.</span><span class="n">put</span><span class="p" data-group-id="2312876431-46">(</span><span class="ss">:method</span><span class="p">,</span><span class="w"> </span><span class="s">&quot;POST&quot;</span><span class="p" data-group-id="2312876431-46">)</span><span class="w">
      </span><span class="o">|&gt;</span><span class="w"> </span><span class="nc">Map</span><span class="o">.</span><span class="n">put</span><span class="p" data-group-id="2312876431-47">(</span><span class="ss">:params</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="2312876431-48">%{</span><span class="s">&quot;a&quot;</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span><span class="w"> </span><span class="s">&quot;b&quot;</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="mi">2</span><span class="p" data-group-id="2312876431-48">}</span><span class="p" data-group-id="2312876431-47">)</span><span class="w">

    </span><span class="p" data-group-id="2312876431-49">%{</span><span class="ss">conn</span><span class="p">:</span><span class="w"> </span><span class="n">conn</span><span class="p" data-group-id="2312876431-49">}</span><span class="w">
  </span><span class="k" data-group-id="2312876431-44">end</span><span class="w">

  </span><span class="kd">defp</span><span class="w"> </span><span class="nf">post_request</span><span class="p" data-group-id="2312876431-50">(</span><span class="n">conn</span><span class="p">,</span><span class="w"> </span><span class="n">callback</span><span class="w"> </span><span class="o">\\</span><span class="w"> </span><span class="no">nil</span><span class="p">,</span><span class="w"> </span><span class="n">params</span><span class="w"> </span><span class="o">\\</span><span class="w"> </span><span class="p" data-group-id="2312876431-51">%{</span><span class="p" data-group-id="2312876431-51">}</span><span class="p" data-group-id="2312876431-50">)</span><span class="w"> </span><span class="k" data-group-id="2312876431-52">do</span><span class="w">
    </span><span class="nc">Process</span><span class="o">.</span><span class="n">put</span><span class="p" data-group-id="2312876431-53">(</span><span class="ss">:callback</span><span class="p">,</span><span class="w"> </span><span class="n">callback</span><span class="p" data-group-id="2312876431-53">)</span><span class="w">

    </span><span class="n">post</span><span class="p" data-group-id="2312876431-54">(</span><span class="n">conn</span><span class="p">,</span><span class="w"> </span><span class="sx">~p&quot;/api/idempotency-plug-handler-test&quot;</span><span class="p">,</span><span class="w"> </span><span class="nc">Map</span><span class="o">.</span><span class="n">merge</span><span class="p" data-group-id="2312876431-55">(</span><span class="p" data-group-id="2312876431-56">%{</span><span class="s">&quot;a&quot;</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="mi">1</span><span class="p" data-group-id="2312876431-56">}</span><span class="p">,</span><span class="w"> </span><span class="n">params</span><span class="p" data-group-id="2312876431-55">)</span><span class="p" data-group-id="2312876431-54">)</span><span class="w">
  </span><span class="k" data-group-id="2312876431-52">end</span><span class="w">
</span><span class="k" data-group-id="2312876431-1">end</span></code></pre>

      ]]>
    </content>
  </entry>
  
  <entry>
    <title>Simple fixtures in ExUnit</title>
    <link href="https://danschultzer.com/posts/simple-fixtures-with-exunit" />
    <id>https://danschultzer.com/posts/simple-fixtures-with-exunit</id>
    <updated>2023-03-01T00:00:00Z</updated>
    <summary>How I roll my own fixtures logic in ExUnit.</summary>
    <content type="html">
      <![CDATA[
        <p>
Years back, when I moved from the Rails world to Elixir, it seemed natural to just use <a href="https://github.com/thoughtbot/ex_machina">ExMachina</a> in place of <a href="https://github.com/thoughtbot/factory_bot">Factory Girl</a> (now called Factory Bot). I didn’t question why I needed a library to set up fixtures, this was <em>just the way</em> to do it.</p>
<p>
I didn’t realize that, out of the box, I had everything needed to set up fixture helpers in ExUnit. ExMachina was unnecessary and mostly just obfuscated stuff.</p>
<h3>
No validations</h3>
<p>
The biggest pain point I experienced is that none of my Ecto validations runs with ExMachina. As my codebase changed I didn’t see bugs surface from my business logic when using ExMachina fixtures. There were also cases of false positives where it wasn’t immediately obvious I needed to update the fixture. Attributes were either not set, or the context function would set attributes differently than the fixture.</p>
<p>
You may find a way to <a href="https://github.com/thoughtbot/ex_machina/issues/211#issuecomment-513876413">hack it</a>, but ultimately for tests to be accurate we need to trust that our data model is accurate. Bleeding the boundary between the data schema and the context API was a recipe for problems down the road.</p>
<h3>
Bloat</h3>
<p>
In Ruby-land <code class="inline">build</code> made sense since you are dealing with object state. Elixir being functional you are dealing with data transformations. Ecto lets the database deal with database decisions, so the data has to get to the database, and you end up either only using <code class="inline">insert</code> or combining <code class="inline">build</code> and <code class="inline">Repo.insert</code> for everything.</p>
<p>
Lists, pairs, and params, are all very trivial to write out in tests. If it’s only a couple of lines to do something then why depend on a library for it? Much better to have concise legible code than abstract it away in a function hidden in a dependency.</p>
<h3>
Obfuscation</h3>
<p>
Defining the function <code class="inline">organization_factory</code> and then calling <code class="inline">insert(:organization)</code> is not easy to reason about. What is this <code class="inline">:organization</code> atom? It would have made a lot more sense with a macro like <code class="inline">factory :organization do</code>.</p>
<p>
Dealing with multiple repos and using the <code class="inline">use ExMachina.Ecto, repo: MyApp.Repo</code> macro, you are forced to find <a href="https://github.com/thoughtbot/ex_machina/issues/217#issuecomment-416772595">workarounds</a>. Like combining <code class="inline">build</code> and <code class="inline">Repo.insert</code>, because the repo is implicitly used in the <code class="inline">insert</code> function.</p>
<h2>
Use the context API</h2>
<p>
A better option is to just use the context functions. We already got everything we need with ExUnit and the Ecto sandbox repo. We can prevent bleeding the boundaries in our tests between the data schema and context API. Our fixtures will be kept explicit, concise, and comprehensible.</p>
<p>
First, we set up a fixtures module calling our context functions:</p>
<pre><code class="makeup elixir"><span class="c1"># test/support/fixtures.ex</span><span class="w">
</span><span class="kd">defmodule</span><span class="w"> </span><span class="nc">MyApp.Fixtures</span><span class="w"> </span><span class="k" data-group-id="0045738967-1">do</span><span class="w">
  </span><span class="kn">alias</span><span class="w"> </span><span class="nc">MyApp.Organizations</span><span class="w">

  </span><span class="na">@organization_attrs</span><span class="w"> </span><span class="p" data-group-id="0045738967-2">%{</span><span class="ss">name</span><span class="p">:</span><span class="w"> </span><span class="s">&quot;Example Organization&quot;</span><span class="p" data-group-id="0045738967-2">}</span><span class="w">

  </span><span class="kd">def</span><span class="w"> </span><span class="nf">organization_fixture</span><span class="p" data-group-id="0045738967-3">(</span><span class="n">attrs</span><span class="w"> </span><span class="o">\\</span><span class="w"> </span><span class="p" data-group-id="0045738967-4">%{</span><span class="p" data-group-id="0045738967-4">}</span><span class="p" data-group-id="0045738967-3">)</span><span class="w"> </span><span class="k" data-group-id="0045738967-5">do</span><span class="w">
    </span><span class="n">attrs</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">Map</span><span class="o">.</span><span class="n">merge</span><span class="p" data-group-id="0045738967-6">(</span><span class="na">@organization_attrs</span><span class="p">,</span><span class="w"> </span><span class="n">attrs</span><span class="p" data-group-id="0045738967-6">)</span><span class="w">

    </span><span class="p" data-group-id="0045738967-7">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="n">organization</span><span class="p" data-group-id="0045738967-7">}</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">Organizations</span><span class="o">.</span><span class="n">create_organization</span><span class="p" data-group-id="0045738967-8">(</span><span class="n">attrs</span><span class="p" data-group-id="0045738967-8">)</span><span class="w">

    </span><span class="n">organization</span><span class="w">
  </span><span class="k" data-group-id="0045738967-5">end</span><span class="w">
</span><span class="k" data-group-id="0045738967-1">end</span></code></pre>
<p>
Then import or alias the fixtures module in the <code class="inline">MyApp.DataCase</code> <code class="inline">using</code> macro:</p>
<pre><code class="makeup elixir"><span class="c1"># test/support/data_case.ex</span><span class="w">
</span><span class="kd">defmodule</span><span class="w"> </span><span class="nc">MyApp.DataCase</span><span class="w"> </span><span class="k" data-group-id="2899268801-1">do</span><span class="w">
  </span><span class="kn">use</span><span class="w"> </span><span class="nc">ExUnit.CaseTemplate</span><span class="w">

  </span><span class="n">using</span><span class="w"> </span><span class="k" data-group-id="2899268801-2">do</span><span class="w">
    </span><span class="k">quote</span><span class="w"> </span><span class="k" data-group-id="2899268801-3">do</span><span class="w">
      </span><span class="kn">alias</span><span class="w"> </span><span class="nc">MyApp.Repo</span><span class="w">

      </span><span class="kn">import</span><span class="w"> </span><span class="nc">Ecto</span><span class="w">
      </span><span class="kn">import</span><span class="w"> </span><span class="nc">Ecto.Changeset</span><span class="w">
      </span><span class="kn">import</span><span class="w"> </span><span class="nc">Ecto.Query</span><span class="w">
      </span><span class="kn">import</span><span class="w"> </span><span class="nc">MyApp.DataCase</span><span class="w">
      </span><span class="kn">import</span><span class="w"> </span><span class="nc">MyApp.Fixtures</span><span class="w"> </span><span class="c1"># or `alias MyApp.Fixtures`</span><span class="w">
    </span><span class="k" data-group-id="2899268801-3">end</span><span class="w">
  </span><span class="k" data-group-id="2899268801-2">end</span></code></pre>
<p>
Now we can use the fixtures in tests:</p>
<pre><code class="makeup elixir"><span class="c1"># test/my_app/organizations_test.exs</span><span class="w">
</span><span class="kd">defmodule</span><span class="w"> </span><span class="nc">MyApp.OrganizationsTest</span><span class="w"> </span><span class="k" data-group-id="4324987167-1">do</span><span class="w">
  </span><span class="kn">use</span><span class="w"> </span><span class="nc">MyApp.DataCase</span><span class="w">

  </span><span class="n">setup</span><span class="w"> </span><span class="k" data-group-id="4324987167-2">do</span><span class="w">
    </span><span class="p" data-group-id="4324987167-3">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="ss">organization</span><span class="p">:</span><span class="w"> </span><span class="n">organization_fixture</span><span class="p" data-group-id="4324987167-4">(</span><span class="p" data-group-id="4324987167-4">)</span><span class="p" data-group-id="4324987167-3">}</span><span class="w">
  </span><span class="k" data-group-id="4324987167-2">end</span><span class="w">

  </span><span class="n">test</span><span class="w"> </span><span class="s">&quot;update_organization/1&quot;</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="4324987167-5">%{</span><span class="ss">organization</span><span class="p">:</span><span class="w"> </span><span class="n">organization</span><span class="p" data-group-id="4324987167-5">}</span><span class="w"> </span><span class="k" data-group-id="4324987167-6">do</span><span class="w">
    </span><span class="c1"># ...</span><span class="w">
  </span><span class="k" data-group-id="4324987167-6">end</span><span class="w">
</span><span class="k" data-group-id="4324987167-1">end</span></code></pre>
<p>
This is how I do it in all projects. It’s been effortless to work with.</p>
<h3>
Drift</h3>
<p>
The benefit of this is that we’ll catch drift right away. <code class="inline">MatchError</code> is raised if the context function fails:</p>
<pre><code class="makeup elixir"><span class="w"> </span><span class="mi">1</span><span class="p">)</span><span class="w"> </span><span class="n">test</span><span class="w"> </span><span class="n">update_organization</span><span class="o">/</span><span class="mi">1</span><span class="w"> </span><span class="p" data-group-id="3749643684-1">(</span><span class="nc">MyApp.OrganizationsTest</span><span class="p" data-group-id="3749643684-1">)</span><span class="w">
     </span><span class="n">test</span><span class="o">/</span><span class="n">my_app</span><span class="o">/</span><span class="n">organizations_test</span><span class="o">.</span><span class="n">exs</span><span class="p">:</span><span class="mi">8</span><span class="w">
     </span><span class="gt">** (MatchError) no match of right hand side value: {:error, #Ecto.Changeset&lt;action: :insert, changes: %{}, errors: [description: {&quot;can&#39;t be blank&quot;, [validation: :required]}], data: #MyApp.Organizations.Organization&lt;&gt;, valid?: false&gt;}
     stacktrace:
       (my_app 0.1.0) test/support/fixtures.ex:8: MyApp.Fixtures.organization_fixture/1
       test/my_app/organizations_test.exs:6: MyApp.OrganizationsTest.__ex_unit_setup_4_0/1
       MyApp.OrganizationsTest.__ex_unit_describe_4/1</span></code></pre>
<h3>
Sequences with unique constraints</h3>
<p>
In the Ecto sandbox transaction, everything is isolated to the individual test processes. That makes it easy to deal with unique constraints. We only need to store an accumulator in the test process dictionary:</p>
<pre><code class="makeup elixir"><span class="c1"># test/support/fixtures.ex</span><span class="w">
</span><span class="kd">defmodule</span><span class="w"> </span><span class="nc">MyApp.Fixtures</span><span class="w"> </span><span class="k" data-group-id="4891142816-1">do</span><span class="w">
  </span><span class="kn">alias</span><span class="w"> </span><span class="nc">MyApp.Organizations</span><span class="w">

  </span><span class="na">@organization_attrs</span><span class="w"> </span><span class="p" data-group-id="4891142816-2">%{</span><span class="ss">name</span><span class="p">:</span><span class="w"> </span><span class="s">&quot;Example Organization&quot;</span><span class="p" data-group-id="4891142816-2">}</span><span class="w">

  </span><span class="kd">def</span><span class="w"> </span><span class="nf">organization_fixture</span><span class="p" data-group-id="4891142816-3">(</span><span class="n">attrs</span><span class="w"> </span><span class="o">\\</span><span class="w"> </span><span class="p" data-group-id="4891142816-4">%{</span><span class="p" data-group-id="4891142816-4">}</span><span class="p" data-group-id="4891142816-3">)</span><span class="w"> </span><span class="k" data-group-id="4891142816-5">do</span><span class="w">
    </span><span class="n">attrs</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">Map</span><span class="o">.</span><span class="n">merge</span><span class="p" data-group-id="4891142816-6">(</span><span class="n">unique_organization_attrs</span><span class="p" data-group-id="4891142816-7">(</span><span class="p" data-group-id="4891142816-7">)</span><span class="p">,</span><span class="w"> </span><span class="n">attrs</span><span class="p" data-group-id="4891142816-6">)</span><span class="w">

    </span><span class="p" data-group-id="4891142816-8">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="n">organization</span><span class="p" data-group-id="4891142816-8">}</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">Organizations</span><span class="o">.</span><span class="n">create_organization</span><span class="p" data-group-id="4891142816-9">(</span><span class="n">attrs</span><span class="p" data-group-id="4891142816-9">)</span><span class="w">

    </span><span class="n">organization</span><span class="w">
  </span><span class="k" data-group-id="4891142816-5">end</span><span class="w">

  </span><span class="kd">defp</span><span class="w"> </span><span class="nf">unique_organization_attrs</span><span class="w"> </span><span class="k" data-group-id="4891142816-10">do</span><span class="w">
    </span><span class="n">n</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">Process</span><span class="o">.</span><span class="n">get</span><span class="p" data-group-id="4891142816-11">(</span><span class="ss">:organization_fixture</span><span class="p">,</span><span class="w"> </span><span class="mi">0</span><span class="p" data-group-id="4891142816-11">)</span><span class="w">
    </span><span class="nc">Process</span><span class="o">.</span><span class="n">put</span><span class="p" data-group-id="4891142816-12">(</span><span class="ss">:organization_fixture</span><span class="p">,</span><span class="w"> </span><span class="n">n</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="mi">1</span><span class="p" data-group-id="4891142816-12">)</span><span class="w">

    </span><span class="nc">Map</span><span class="o">.</span><span class="n">put</span><span class="p" data-group-id="4891142816-13">(</span><span class="na">@organization_attrs</span><span class="p">,</span><span class="w"> </span><span class="ss">:email</span><span class="p">,</span><span class="w"> </span><span class="s">&quot;test-</span><span class="si" data-group-id="4891142816-14">#{</span><span class="n">n</span><span class="si" data-group-id="4891142816-14">}</span><span class="s">@example.com&quot;</span><span class="p" data-group-id="4891142816-13">)</span><span class="w">
  </span><span class="k" data-group-id="4891142816-10">end</span><span class="w">
</span><span class="k" data-group-id="4891142816-1">end</span></code></pre>
<h3>
Associations</h3>
<p>
Expose belong-to associations in the fixture function header:</p>
<pre><code class="makeup elixir"><span class="c1"># test/support/fixtures.ex</span><span class="w">
</span><span class="kd">defmodule</span><span class="w"> </span><span class="nc">MyApp.Fixtures</span><span class="w"> </span><span class="k" data-group-id="8754073684-1">do</span><span class="w">
  </span><span class="c1"># ...</span><span class="w">

  </span><span class="kd">def</span><span class="w"> </span><span class="nf">account_fixture</span><span class="p" data-group-id="8754073684-2">(</span><span class="n">organization</span><span class="p">,</span><span class="w"> </span><span class="n">attrs</span><span class="w"> </span><span class="o">\\</span><span class="w"> </span><span class="p" data-group-id="8754073684-3">%{</span><span class="p" data-group-id="8754073684-3">}</span><span class="p" data-group-id="8754073684-2">)</span><span class="w"> </span><span class="k" data-group-id="8754073684-4">do</span><span class="w">
    </span><span class="n">attrs</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">Map</span><span class="o">.</span><span class="n">merge</span><span class="p" data-group-id="8754073684-5">(</span><span class="na">@account_attrs</span><span class="p">,</span><span class="w"> </span><span class="n">attrs</span><span class="p" data-group-id="8754073684-5">)</span><span class="w">

    </span><span class="p" data-group-id="8754073684-6">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="n">account</span><span class="p" data-group-id="8754073684-6">}</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">Organizations</span><span class="o">.</span><span class="n">create_account</span><span class="p" data-group-id="8754073684-7">(</span><span class="n">organization</span><span class="p">,</span><span class="w"> </span><span class="n">attrs</span><span class="p" data-group-id="8754073684-7">)</span><span class="w">

    </span><span class="n">account</span><span class="w">
  </span><span class="k" data-group-id="8754073684-4">end</span><span class="w">
</span><span class="k" data-group-id="8754073684-1">end</span></code></pre>
<p>
In tests we will call both fixture functions:</p>
<pre><code class="makeup elixir"><span class="c1"># test/my_app/organizations_test.exs</span><span class="w">
</span><span class="kd">defmodule</span><span class="w"> </span><span class="nc">MyApp.OrganizationsTest</span><span class="w"> </span><span class="k" data-group-id="0589965293-1">do</span><span class="w">
  </span><span class="kn">use</span><span class="w"> </span><span class="nc">MyApp.DataCase</span><span class="w">

  </span><span class="c1"># ...</span><span class="w">

  </span><span class="n">describe</span><span class="w"> </span><span class="s">&quot;update_account/2&quot;</span><span class="w"> </span><span class="k" data-group-id="0589965293-2">do</span><span class="w">
    </span><span class="n">setup</span><span class="w"> </span><span class="k" data-group-id="0589965293-3">do</span><span class="w">
      </span><span class="n">organization</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">organization_fixture</span><span class="p" data-group-id="0589965293-4">(</span><span class="p" data-group-id="0589965293-4">)</span><span class="w">
      </span><span class="n">account</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">account_fixture</span><span class="p" data-group-id="0589965293-5">(</span><span class="n">organization</span><span class="p" data-group-id="0589965293-5">)</span><span class="w">

      </span><span class="p" data-group-id="0589965293-6">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="ss">organization</span><span class="p">:</span><span class="w"> </span><span class="n">organization</span><span class="p">,</span><span class="w"> </span><span class="ss">account</span><span class="p">:</span><span class="w"> </span><span class="n">account</span><span class="p" data-group-id="0589965293-6">}</span><span class="w">
    </span><span class="k" data-group-id="0589965293-3">end</span><span class="w">

    </span><span class="n">test</span><span class="w"> </span><span class="s">&quot;updates&quot;</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="0589965293-7">%{</span><span class="ss">account</span><span class="p">:</span><span class="w"> </span><span class="n">account</span><span class="p" data-group-id="0589965293-7">}</span><span class="w"> </span><span class="k" data-group-id="0589965293-8">do</span><span class="w">
      </span><span class="c1"># ...</span><span class="w">
    </span><span class="k" data-group-id="0589965293-8">end</span><span class="w">
  </span><span class="k" data-group-id="0589965293-2">end</span><span class="w">
</span><span class="k" data-group-id="0589965293-1">end</span></code></pre>
<p>
There’s no need to be clever about this. Don’t make <code class="inline">account_fixture()</code> automatically create the belong-to association. We would end up having to pass in a keyword list or optional argument when creating multiple fixtures with the same association. Save yourself the trouble. Keep it boring and explicit!</p>
<h3>
Lists, pairs, params</h3>
<p>
We can just use comprehension in the test to create the list or pairs of fixtures:</p>
<pre><code class="makeup elixir"><span class="c1"># test/my_app/organizations_test.exs</span><span class="w">
</span><span class="kd">defmodule</span><span class="w"> </span><span class="nc">MyApp.OrganizationsTest</span><span class="w"> </span><span class="k" data-group-id="0246835900-1">do</span><span class="w">
  </span><span class="kn">use</span><span class="w"> </span><span class="nc">MyApp.DataCase</span><span class="w">

  </span><span class="c1"># ...</span><span class="w">

  </span><span class="n">describe</span><span class="w"> </span><span class="s">&quot;list_accounts/0&quot;</span><span class="w"> </span><span class="k" data-group-id="0246835900-2">do</span><span class="w">
    </span><span class="n">setup</span><span class="w"> </span><span class="k" data-group-id="0246835900-3">do</span><span class="w">
      </span><span class="n">accounts</span><span class="w"> </span><span class="o">=</span><span class="w">
        </span><span class="k">for</span><span class="w"> </span><span class="bp">_</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="mi">1</span><span class="o">..</span><span class="mi">10</span><span class="w"> </span><span class="k" data-group-id="0246835900-4">do</span><span class="w">
          </span><span class="n">account_fixture</span><span class="p" data-group-id="0246835900-5">(</span><span class="n">organization_fixture</span><span class="p" data-group-id="0246835900-6">(</span><span class="p" data-group-id="0246835900-6">)</span><span class="p" data-group-id="0246835900-5">)</span><span class="w">
        </span><span class="k" data-group-id="0246835900-4">end</span><span class="w">

      </span><span class="p" data-group-id="0246835900-7">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="ss">accounts</span><span class="p">:</span><span class="w"> </span><span class="n">accounts</span><span class="p" data-group-id="0246835900-7">}</span><span class="w">
    </span><span class="k" data-group-id="0246835900-3">end</span><span class="w">

    </span><span class="n">test</span><span class="w"> </span><span class="s">&quot;lists&quot;</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="0246835900-8">%{</span><span class="ss">accounts</span><span class="p">:</span><span class="w"> </span><span class="n">accounts</span><span class="p" data-group-id="0246835900-8">}</span><span class="w"> </span><span class="k" data-group-id="0246835900-9">do</span><span class="w">
      </span><span class="c1"># ...</span><span class="w">
    </span><span class="k" data-group-id="0246835900-9">end</span><span class="w">
  </span><span class="k" data-group-id="0246835900-2">end</span><span class="w">
</span><span class="k" data-group-id="0246835900-1">end</span></code></pre>
<p>
If we need to generate params then we can expose the defaults as a function:</p>
<pre><code class="makeup elixir"><span class="c1"># test/support/fixtures.ex</span><span class="w">
</span><span class="kd">defmodule</span><span class="w"> </span><span class="nc">MyApp.Fixtures</span><span class="w"> </span><span class="k" data-group-id="1660904787-1">do</span><span class="w">
  </span><span class="kn">alias</span><span class="w"> </span><span class="nc">MyApp.Organizations</span><span class="w">

  </span><span class="na">@organization_attrs</span><span class="w"> </span><span class="p" data-group-id="1660904787-2">%{</span><span class="ss">name</span><span class="p">:</span><span class="w"> </span><span class="s">&quot;Example Organization&quot;</span><span class="p" data-group-id="1660904787-2">}</span><span class="w">

  </span><span class="kd">def</span><span class="w"> </span><span class="nf">organization_attrs</span><span class="p">,</span><span class="w"> </span><span class="ss">do</span><span class="p">:</span><span class="w"> </span><span class="na">@organization_attrs</span><span class="w">

  </span><span class="c1"># ...</span><span class="w">
</span><span class="k" data-group-id="1660904787-1">end</span></code></pre>
<hr class="thin">
<p>
We tend to believe that we should use a dependency because everybody else is using it. It’s good to take a step back and question if a dependency makes sense for your project. Rolling your own can often be the right answer if you only end up using a minimal amount of the dependency.</p>

      ]]>
    </content>
  </entry>
  
  <entry>
    <title>TestServer - mock third-party services</title>
    <link href="https://danschultzer.com/posts/testserver-mock-third-party-services" />
    <id>https://danschultzer.com/posts/testserver-mock-third-party-services</id>
    <updated>2023-02-23T00:00:00Z</updated>
    <summary>The motivations behind building TestServer for ExUnit.</summary>
    <content type="html">
      <![CDATA[
        <p>
For a long time, I’ve depended on <a href="https://github.com/PSPDFKit-labs/bypass"><code class="inline">Bypass</code></a> to mock third-party services in Elixir. You only really need this when you have to test the HTTP client implementation, otherwise, you could (and probably should) just mock the HTTP client itself.</p>
<h2>
<code class="inline">Assent</code></h2>
<p>
<a href="https://github.com/pow-auth/assent"><code class="inline">Assent</code></a> implements HTTP client adapters for <a href="https://www.erlang.org/doc/man/httpc.html"><code class="inline">:httpc</code></a> and <a href="https://github.com/elixir-mint/mint"><code class="inline">Mint</code></a>.</p>
<p>
<code class="inline">:httpc</code> doesn’t validate TLS certificates out of the box. To enable validation you need to configure <code class="inline">:httpc</code>. This means that to ensure the configuration works, I must test against an HTTPS endpoint. Unfortunately, <code class="inline">Bypass</code> doesn’t, and <a href="https://github.com/PSPDFKit-labs/bypass/issues/63#issuecomment-638110757">won’t</a>, support TLS. This also prevents testing <a href="https://github.com/PSPDFKit-labs/bypass/pull/92#issuecomment-638130376">HTTP/2</a> (and HTTP/3) connections.</p>
<p>
To ensure that the <code class="inline">:httpc</code> configuration worked, I added unit tests that send off requests to <a href="https://badssl.com">badssl.com</a>. However, with CI depending on an external service, I might have to deal with network issues causing it to fail, plus added latency. Ideally, test suites run without any external dependencies.</p>
<p>
Last year I fixed this by setting up a <a href="https://github.com/pow-auth/assent/pull/99">custom mock service</a>. I had all I needed with HTTP/2 and TLS support!</p>
<h2>
JSON-RPC and GraphQL</h2>
<p>
A while later I had to mock a JSON-RPC third-party service. I discovered another flaw in <code class="inline">Bypass</code>, which also existed in the custom mock service in <code class="inline">Assent</code>.</p>
<p>
JSON-RPC only has one endpoint path. Both <code class="inline">Bypass</code> and the custom mock service in <code class="inline">Assent</code> can only mock a path once. This would make it impossible to mock a GraphQL endpoint as well, if you have more than one request happening in a test.</p>
<p>
With <code class="inline">Bypass</code>, you could get around this by using the <code class="inline">expect</code> function since it allows for any number of requests to the same path. But that doesn’t feel right. We want to know exactly how many requests to expect for a test. And it feels messy to deal with state inside the callback function to know how many requests have been received.</p>
<h2>
<code class="inline">TestServer</code></h2>
<p>
Fed up with these issues I went to work. I wanted a flexible mocking library that could be used in most, if not all, third-party service scenarios. The result is <a href="https://github.com/danschultzer/test_server"><code class="inline">TestServer</code></a>. A plug-based mocking library that doesn’t get in your way.</p>
<h4>
FIFO queue</h4>
<p>
Instead of using the path as a unique identifier to match, I built a FIFO queue of request expectations. Each request expectation can be customized to match any attributes for a request (i.e. path, method, any attributes in the <code class="inline">%Plug.Conn{}</code> struct):</p>
<pre><code class="makeup elixir"><span class="nc">TestServer</span><span class="o">.</span><span class="n">add</span><span class="p" data-group-id="9438718605-1">(</span><span class="s">&quot;/&quot;</span><span class="p">,</span><span class="w"> </span><span class="ss">via</span><span class="p">:</span><span class="w"> </span><span class="ss">:get</span><span class="p">,</span><span class="w"> </span><span class="ss">match</span><span class="p">:</span><span class="w"> </span><span class="k" data-group-id="9438718605-2">fn</span><span class="w">
  </span><span class="p" data-group-id="9438718605-3">%{</span><span class="ss">params</span><span class="p">:</span><span class="w"> </span><span class="p" data-group-id="9438718605-4">%{</span><span class="s">&quot;a&quot;</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="mi">1</span><span class="p" data-group-id="9438718605-4">}</span><span class="p" data-group-id="9438718605-3">}</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="c">_conn</span><span class="w"> </span><span class="o">-&gt;</span><span class="w"> </span><span class="no">true</span><span class="w">
  </span><span class="c">_conn</span><span class="w"> </span><span class="o">-&gt;</span><span class="w"> </span><span class="no">false</span><span class="w">
</span><span class="k" data-group-id="9438718605-2">end</span><span class="p" data-group-id="9438718605-1">)</span></code></pre>
<p>
It’s also possible to transform requests before it matches expectations:</p>
<pre><code class="makeup elixir"><span class="nc">TestServer</span><span class="o">.</span><span class="n">plug</span><span class="p" data-group-id="4138583591-1">(</span><span class="k" data-group-id="4138583591-2">fn</span><span class="w"> </span><span class="n">conn</span><span class="w"> </span><span class="o">-&gt;</span><span class="w">
  </span><span class="p" data-group-id="4138583591-3">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="n">body</span><span class="p">,</span><span class="w"> </span><span class="c">_conn</span><span class="p" data-group-id="4138583591-3">}</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">Plug.Conn</span><span class="o">.</span><span class="n">read_body</span><span class="p" data-group-id="4138583591-4">(</span><span class="n">conn</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="4138583591-5">[</span><span class="p" data-group-id="4138583591-5">]</span><span class="p" data-group-id="4138583591-4">)</span><span class="w">
  </span><span class="p" data-group-id="4138583591-6">%{</span><span class="n">conn</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="ss">body_params</span><span class="p">:</span><span class="w"> </span><span class="nc">Jason</span><span class="o">.</span><span class="n">decode!</span><span class="p" data-group-id="4138583591-7">(</span><span class="n">body</span><span class="p" data-group-id="4138583591-7">)</span><span class="p" data-group-id="4138583591-6">}</span><span class="w">
</span><span class="k" data-group-id="4138583591-2">end</span><span class="p" data-group-id="4138583591-1">)</span></code></pre>
<p>
As <code class="inline">TestServer</code> is plug based, you can also use any plugs in your app:</p>
<pre><code class="makeup elixir"><span class="nc">TestServer</span><span class="o">.</span><span class="n">plug</span><span class="p" data-group-id="3378182822-1">(</span><span class="nc">FetchBodyPlug</span><span class="p" data-group-id="3378182822-1">)</span><span class="w">
</span><span class="nc">TestServer</span><span class="o">.</span><span class="n">add</span><span class="p" data-group-id="3378182822-2">(</span><span class="s">&quot;/&quot;</span><span class="p">,</span><span class="w"> </span><span class="ss">to</span><span class="p">:</span><span class="w"> </span><span class="nc">MyPlug</span><span class="p" data-group-id="3378182822-2">)</span></code></pre>
<p>
<code class="inline">TestServer</code> will fail if it receives an unexpected request:</p>
<pre><code class="makeup elixir"><span class="w">  </span><span class="mi">1</span><span class="p">)</span><span class="w"> </span><span class="n">test</span><span class="w"> </span><span class="n">fails</span><span class="w"> </span><span class="p" data-group-id="5369582320-1">(</span><span class="nc">Test</span><span class="p" data-group-id="5369582320-1">)</span><span class="w">
     </span><span class="n">test</span><span class="o">/</span><span class="n">test</span><span class="o">.</span><span class="n">exs</span><span class="p">:</span><span class="mi">10</span><span class="w">
     </span><span class="gt">** (RuntimeError) TestServer.Instance #PID&lt;0.350.0&gt; received an unexpected GET request at /path.
     
     Active routes:
     
     #1: * /
         test/test.exs:10: Test.&quot;test fails&quot;/1</span></code></pre>
<p>
<code class="inline">TestServer</code> also fails if request expectations went unmatched:</p>
<pre><code class="makeup elixir"><span class="w">  </span><span class="mi">1</span><span class="p">)</span><span class="w"> </span><span class="n">test</span><span class="w"> </span><span class="n">fails</span><span class="w"> </span><span class="p" data-group-id="2311513737-1">(</span><span class="nc">Test</span><span class="p" data-group-id="2311513737-1">)</span><span class="w">
     </span><span class="n">test</span><span class="o">/</span><span class="n">test</span><span class="o">.</span><span class="n">exs</span><span class="p">:</span><span class="mi">10</span><span class="w">
    </span><span class="gt">** (RuntimeError) TestServer.Instance #PID&lt;0.340.0&gt; did not receive a request for these routes before the test ended:
     
     #1: * /
         test/test.exs:10: Test.&quot;test fails&quot;/1</span></code></pre>
<h4>
Self-signed certificate</h4>
<p>
To make it easier to test TLS, the server can automatically generate a self-signed certificate with the <a href="https://github.com/voltone/x509"><code class="inline">X509</code> library</a>. The certificate can be passed onto the HTTP client:</p>
<pre><code class="makeup elixir"><span class="p" data-group-id="6126016396-1">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="n">instance</span><span class="p" data-group-id="6126016396-1">}</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">TestServer</span><span class="o">.</span><span class="n">start</span><span class="p" data-group-id="6126016396-2">(</span><span class="ss">scheme</span><span class="p">:</span><span class="w"> </span><span class="ss">:https</span><span class="p" data-group-id="6126016396-2">)</span><span class="w">

</span><span class="n">assert</span><span class="w"> </span><span class="p" data-group-id="6126016396-3">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="6126016396-4">{</span><span class="p" data-group-id="6126016396-5">{</span><span class="bp">_</span><span class="p">,</span><span class="w"> </span><span class="mi">200</span><span class="p">,</span><span class="w"> </span><span class="bp">_</span><span class="p" data-group-id="6126016396-5">}</span><span class="p">,</span><span class="w"> </span><span class="n">body</span><span class="p" data-group-id="6126016396-4">}</span><span class="p" data-group-id="6126016396-3">}</span><span class="w"> </span><span class="o">=</span><span class="w">
  </span><span class="nc">:httpc</span><span class="o">.</span><span class="n">request</span><span class="p" data-group-id="6126016396-6">(</span><span class="w">
    </span><span class="ss">:get</span><span class="p">,</span><span class="w">
    </span><span class="p" data-group-id="6126016396-7">{</span><span class="nc">String</span><span class="o">.</span><span class="n">to_charlist</span><span class="p" data-group-id="6126016396-8">(</span><span class="nc">TestServer</span><span class="o">.</span><span class="n">url</span><span class="p" data-group-id="6126016396-9">(</span><span class="p" data-group-id="6126016396-9">)</span><span class="p" data-group-id="6126016396-8">)</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="6126016396-10">[</span><span class="p" data-group-id="6126016396-10">]</span><span class="p" data-group-id="6126016396-7">}</span><span class="p">,</span><span class="w">
    </span><span class="ss">ssl</span><span class="p">:</span><span class="w"> </span><span class="p" data-group-id="6126016396-11">[</span><span class="w">
      </span><span class="ss">verify</span><span class="p">:</span><span class="w"> </span><span class="ss">:verify_peer</span><span class="p">,</span><span class="w">
      </span><span class="ss">cacerts</span><span class="p">:</span><span class="w"> </span><span class="nc">TestServer</span><span class="o">.</span><span class="n">x509_suite</span><span class="p" data-group-id="6126016396-12">(</span><span class="p" data-group-id="6126016396-12">)</span><span class="o">.</span><span class="n">cacerts</span><span class="p">,</span><span class="w">
      </span><span class="c1"># ...</span><span class="w">
    </span><span class="p" data-group-id="6126016396-11">]</span><span class="p" data-group-id="6126016396-6">)</span></code></pre>
<h4>
WebSocket</h4>
<p>
<code class="inline">TestServer</code> can also mock WebSocket connections and handle bidirectional messages:</p>
<pre><code class="makeup elixir"><span class="p" data-group-id="5290004438-1">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="n">socket</span><span class="p" data-group-id="5290004438-1">}</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">TestServer</span><span class="o">.</span><span class="n">websocket_init</span><span class="p" data-group-id="5290004438-2">(</span><span class="s">&quot;/ws&quot;</span><span class="p" data-group-id="5290004438-2">)</span><span class="w">

</span><span class="ss">:ok</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">TestServer</span><span class="o">.</span><span class="n">websocket_handle</span><span class="p" data-group-id="5290004438-3">(</span><span class="w">
  </span><span class="n">socket</span><span class="p">,</span><span class="w">
  </span><span class="ss">match</span><span class="p">:</span><span class="w"> </span><span class="k" data-group-id="5290004438-4">fn</span><span class="w"> </span><span class="p" data-group-id="5290004438-5">{</span><span class="ss">:text</span><span class="p">,</span><span class="w"> </span><span class="n">message</span><span class="p" data-group-id="5290004438-5">}</span><span class="p">,</span><span class="w"> </span><span class="c">_state</span><span class="w"> </span><span class="o">-&gt;</span><span class="w">
    </span><span class="n">message</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="s">&quot;hi&quot;</span><span class="w">
  </span><span class="k" data-group-id="5290004438-4">end</span><span class="p">,</span><span class="w">
  </span><span class="ss">to</span><span class="p">:</span><span class="w"> </span><span class="k" data-group-id="5290004438-6">fn</span><span class="w"> </span><span class="c">_frame</span><span class="p">,</span><span class="w"> </span><span class="n">state</span><span class="w"> </span><span class="o">-&gt;</span><span class="w">
    </span><span class="p" data-group-id="5290004438-7">{</span><span class="ss">:reply</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="5290004438-8">{</span><span class="ss">:text</span><span class="p">,</span><span class="w"> </span><span class="s">&quot;hello&quot;</span><span class="p" data-group-id="5290004438-8">}</span><span class="p">,</span><span class="w"> </span><span class="n">state</span><span class="p" data-group-id="5290004438-7">}</span><span class="w">
  </span><span class="k" data-group-id="5290004438-6">end</span><span class="p" data-group-id="5290004438-3">)</span><span class="w">

</span><span class="ss">:ok</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">TestServer</span><span class="o">.</span><span class="n">websocket_info</span><span class="p" data-group-id="5290004438-9">(</span><span class="n">socket</span><span class="p">,</span><span class="w"> </span><span class="k" data-group-id="5290004438-10">fn</span><span class="w"> </span><span class="n">state</span><span class="w"> </span><span class="o">-&gt;</span><span class="w">
  </span><span class="p" data-group-id="5290004438-11">{</span><span class="ss">:reply</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="5290004438-12">{</span><span class="ss">:text</span><span class="p">,</span><span class="w"> </span><span class="s">&quot;hey!&quot;</span><span class="p" data-group-id="5290004438-12">}</span><span class="p">,</span><span class="w"> </span><span class="n">state</span><span class="p" data-group-id="5290004438-11">}</span><span class="w">
</span><span class="k" data-group-id="5290004438-10">end</span><span class="p" data-group-id="5290004438-9">)</span></code></pre>
<h4>
HTTP Server</h4>
<p>
Under the hood, <code class="inline">TestServer</code> uses <a href="https://github.com/mtrudel/bandit"><code class="inline">Bandit</code></a>, <a href="https://github.com/elixir-plug/plug_cowboy"><code class="inline">Plug.Cowboy</code></a>, or built-in <a href="https://www.erlang.org/doc/man/httpd.html"><code class="inline">:httpd</code></a>, depending on which one is available in your project. When there’s a native Erlang/Elixir HTTP/3 server I’ll include that as well!</p>
<hr class="thin">
<p>
I’m now using <code class="inline">TestServer</code> for <code class="inline">Assent</code> and <a href="https://github.com/danschultzer/premailex"><code class="inline">Premailex</code></a>, plus all private projects where I need to mock third-party services!</p>

      ]]>
    </content>
  </entry>
  
  <entry>
    <title>Rewrites: The perils of not knowing what you don't know</title>
    <link href="https://danschultzer.com/posts/rewrites-perils-of-not-knowing-what-you-dont-know" />
    <id>https://danschultzer.com/posts/rewrites-perils-of-not-knowing-what-you-dont-know</id>
    <updated>2023-02-21T00:00:00Z</updated>
    <summary>As software engineers, there's a trap that we all may fall into if we ignore early warning signs.</summary>
    <content type="html">
      <![CDATA[
        <p>
You are dealing with an awful legacy codebase. It was not properly designed or maintained, there was no clear direction when it was built, and everything is held together with duct tape. It seems like it’s about to collapse at any point. Fixing a bug or adding a feature ends up taking so much time that you think of what choices you made in life that make you deserve to live through this hell.</p>
<p>
The thought constantly comes up. Is all this effort better spent on just throwing out this awful system and building a new thing? Tools have improved considerably since this repo was started in 2000 and late. How hard can it really be?</p>
<p>
<strong>Careful now, you are entering a danger zone!</strong></p>
<h2>
The trap of ignorance</h2>
<p>
You start from scratch. It’s a breath of fresh air to not have to deal with the awfulness of the legacy codebase. You don’t want to repeat the mistakes of the poor souls who built this monstrosity so you are doing your best to find out what’s best practice.</p>
<p>
<strong>You are now falling into the first trap!</strong></p>
<p>
It seems that the answer lies in what others, especially big companies, do. Maybe it’s a microservice structure. Maybe it’s a certain stack. Could be a design philosophy. Could be a language. You read blog posts about how they solved issues (at scale).</p>
<p>
This is the trap: What works for them won’t work for you. I mean it may, but nearly always it doesn’t. Why? Because the problem they are solving is not the problem you are trying to solve. If you take a step back you’ll realize this. Work from first principles, and figure out what will be less effort. Maybe a refactor, as painful as it is, will do wonders.</p>
<p>
But, you’ve already fallen into the trap. Everyone is excited, everyone is on board. Legacy is boring, greenfield is fun. Time to get to work!</p>
<h2>
The trap of sunk cost</h2>
<p>
With a bright future, you have been working on pouring a new foundation. You got to a pretty good place by now. Sure it’s been a while, some things did take a lot longer than you expected. But you got to think about scale and avoid the problems of the legacy codebase! That legacy codebase is a convoluted mess.</p>
<p>
You just have to get past this painful transition, and tomorrow will be better. You’ll be able to move faster and easier when this is done. Just implement the features and you are good.</p>
<p>
<strong>Here lies the second trap!</strong></p>
<p>
It’s easy to blame others with the benefit of hindsight. Yeah, the ones who engineered the legacy codebase obviously didn’t know what they were doing. But do you? Why is it so difficult to maintain it in the first place? In the attempt to make life easier, are you actually adding more complexity to a problem? Did you ask the right questions at the start? Is your focus on the problems you want to avoid blinding you from the problems you are creating?</p>
<p>
With significant time invested, and expectations made, it’s very hard to pull the brakes. If you didn’t take a step back, and work from first principles, how would you know what problem you actually are trying to solve in the legacy codebase? By now you are going down a path where you’ll have to deal with legacy business logic in a new system.</p>
<p>
All the experience built into the legacy codebase will have to be extracted. And you are going to miss something. Because the old codebase didn’t have exhaustive testing, no good boundaries, logic all over the place, the list goes on. It will be very difficult to confirm if you’ve implemented a feature correctly.</p>
<p>
Well, we can’t ponder long on this. Got deadlines to make, impressions to make - most of all to yourself!</p>
<h2>
Blowing up</h2>
<p>
As time goes on this may blow up in your face. It becomes too difficult to reach an acceptable production state. Unmaintainable, it’s just dealing with two monstrosities instead of one. At least the first one did kinda work. The new one seems to break down in random places as network data is fed into it. It’s painful.</p>
<p>
You are burned out.</p>
<h2>
Fizzle out</h2>
<p>
It may also just fizzle out. It works, has some kinks, and you do your best to keep it running. Until you no longer are maintaining it and now a new person or team is complaining about how this codebase was built by idiots. Because you had to support weird legacy business logic. You ended up in a no better situation than before embarking on this.</p>
<h2>
What did we learn?</h2>
<p>
Not a damn thing. We dug our own grave because we didn’t take the time to assess the problem at hand. It may not even have been a problem, or it was an entirely different problem to begin with.</p>
<p>
Lessons learned by experience. We don’t know what we don’t know. The dragons were out there but we didn’t even know there were dragons! At least we didn’t spend too long… wait, it took over eight months?! Oh, oh no.</p>
<h2>
“But wait, it did work out for me”</h2>
<p>
Congrats! You got to the other side. Maybe you assessed the problem right. Maybe you knew what you were doing. Maybe it was dumb luck that you didn’t fall into the pit. Would you make this gamble again?</p>
<h2>
How to approach rewrites</h2>
<p>
Much has been said about rewrites. There are books and blog posts that will go much more into the details and better highlight how to approach rewrites. These are a few things I usually think about when I approach the rewrite itch:</p>
<h4>
1. Know your stuff</h4>
<ul>
  <li>
If you are very comfortable with the stack (e.g. <a href="https://mcfunley.com/choose-boring-technology">“choose boring”</a>) you will prevent quickly drowning in issues. The work required will still be pretty painful.  </li>
  <li>
If you have the experience and introspection to assess the problem, and understand what you really need to solve, you can confidently figure out if a rewrite is worth it. Understanding the emotions behind the itch is important.  </li>
  <li>
If you neither have the experience nor the introspection, then it’s preferable to just deal with the current codebase. The resources are better spent building new features and repairing broken windows.  </li>
</ul>
<h4>
2. The code is not you</h4>
<ul>
  <li>
Be ready to throw away all the work if it doesn’t solve the problem.  </li>
  <li>
Be ready to throw away all the work if you can’t solve it in an acceptable timeframe.  </li>
  <li>
The code is not you. It’s natural to be emotionally attached to your work, but results should be what guides you.  </li>
</ul>
<h4>
3. Results are the only metric</h4>
<ul>
  <li>
Instead of solving big problems, solve small problems. You’ll deliver results much faster. And it may help you identify and tackle bigger underlying issues.  </li>
  <li>
Set appropriate constraints. An attainable hard deadline will help define the problem to solve.  </li>
  <li>
Double your time estimates. Be very careful if it will take more than a few months. Often that can be a sign there is not sufficient understanding of the process.  </li>
</ul>

      ]]>
    </content>
  </entry>
  
  <entry>
    <title>Adding XML feeds to Phoenix</title>
    <link href="https://danschultzer.com/posts/adding-xml-feeds-to-phoenix" />
    <id>https://danschultzer.com/posts/adding-xml-feeds-to-phoenix</id>
    <updated>2023-02-16T00:00:00Z</updated>
    <summary>How I set up generic XML document rendering used for an Atom XML feed with NimblePublisher.</summary>
    <content type="html">
      <![CDATA[
        <p>
Today I was adding an <a href="https://en.wikipedia.org/wiki/Atom_(web_standard)">Atom XML feed</a> to <a href="/posts/welcome-to-my-blog">my</a> <a href="/posts/dynamic-image-generation-with-elixir">blog</a>. Atom is used for feed syndication and is a <a href="https://en.wikipedia.org/wiki/Atom_(web_standard)#Atom_compared_to_RSS_2.0">more robust version</a> of the well-known RSS feed.</p>
<p>
My previous process for finding blog posts was to randomly discover them through social media feeds. When I moved over to Mastodon I realized how off that was. I was missing out on content just because the algorithm didn’t prioritize it! So now I’m using syndication with <a href="https://feedly.com">Feedly</a> to catch up on the blogs I like.</p>
<p>
Let’s add a dynamically generated Atom feed to this blog!</p>
<h2>
The XML EEx sigil</h2>
<p>
I could just use <a href="https://hex.pm/packages/atomex"><code class="inline">atomex</code></a> to generate it in a plug or as a file in <code class="inline">priv/static/</code>. But what if we could have something that feels a little more like good ol’ Phoenix 1.7?</p>
<ul>
  <li>
XML template defined in a sigil like HEEx  </li>
  <li>
XML template with compile-time warnings/errors  </li>
  <li>
XML-template generates the <code class="inline">atom.xml</code> file content or plug response body  </li>
</ul>
<p>
Keeping it as generic XML is also nice since we can reuse it for other features like XML sitemaps or RSS. First, let’s start with how we want the Atom XML module to function:</p>
<pre><code class="makeup elixir"><span class="c1"># lib/my_blog_web/atom_xml.ex</span><span class="w">
</span><span class="kd">defmodule</span><span class="w"> </span><span class="nc">MyBlogWeb.AtomXML</span><span class="w"> </span><span class="k" data-group-id="0509570109-1">do</span><span class="w">
  </span><span class="kn">use</span><span class="w"> </span><span class="nc">MyBlogWeb</span><span class="p">,</span><span class="w"> </span><span class="ss">:xml</span><span class="w">

  </span><span class="kd">def</span><span class="w"> </span><span class="nf">load</span><span class="p" data-group-id="0509570109-2">(</span><span class="bp">_</span><span class="p" data-group-id="0509570109-2">)</span><span class="w"> </span><span class="k" data-group-id="0509570109-3">do</span><span class="w">
    </span><span class="c1"># Atom feed MUST have ISO DateTime format</span><span class="w">
    </span><span class="p" data-group-id="0509570109-4">[</span><span class="n">latest_post</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="bp">_</span><span class="p" data-group-id="0509570109-4">]</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">posts</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">Enum</span><span class="o">.</span><span class="n">map</span><span class="p" data-group-id="0509570109-5">(</span><span class="nc">MyBlog.Blog</span><span class="o">.</span><span class="n">list_posts</span><span class="p" data-group-id="0509570109-6">(</span><span class="p" data-group-id="0509570109-6">)</span><span class="p">,</span><span class="w"> </span><span class="o">&amp;</span><span class="p" data-group-id="0509570109-7">%{</span><span class="ni">&amp;1</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="ss">date</span><span class="p">:</span><span class="w"> </span><span class="n">to_datetime</span><span class="p" data-group-id="0509570109-8">(</span><span class="ni">&amp;1</span><span class="o">.</span><span class="n">date</span><span class="p" data-group-id="0509570109-8">)</span><span class="p" data-group-id="0509570109-7">}</span><span class="p" data-group-id="0509570109-5">)</span><span class="w">

    </span><span class="p" data-group-id="0509570109-9">%{</span><span class="w">
      </span><span class="ss">title</span><span class="p">:</span><span class="w"> </span><span class="s">&quot;Dan Schultzer&quot;</span><span class="p">,</span><span class="w">
      </span><span class="ss">author</span><span class="p">:</span><span class="w"> </span><span class="s">&quot;Dan Schultzer&quot;</span><span class="p">,</span><span class="w">
      </span><span class="ss">updated_at</span><span class="p">:</span><span class="w"> </span><span class="n">latest_post</span><span class="o">.</span><span class="n">date</span><span class="p">,</span><span class="w">
      </span><span class="ss">posts</span><span class="p">:</span><span class="w"> </span><span class="n">posts</span><span class="w">
    </span><span class="p" data-group-id="0509570109-9">}</span><span class="w">
  </span><span class="k" data-group-id="0509570109-3">end</span><span class="w">

  </span><span class="kd">def</span><span class="w"> </span><span class="nf">to_datetime</span><span class="p" data-group-id="0509570109-10">(</span><span class="n">date</span><span class="p" data-group-id="0509570109-10">)</span><span class="w"> </span><span class="k" data-group-id="0509570109-11">do</span><span class="w">
    </span><span class="nc">DateTime</span><span class="o">.</span><span class="n">new!</span><span class="p" data-group-id="0509570109-12">(</span><span class="n">date</span><span class="p">,</span><span class="w"> </span><span class="ld">~T[00:00:00]</span><span class="p" data-group-id="0509570109-12">)</span><span class="w">
  </span><span class="k" data-group-id="0509570109-11">end</span><span class="w">

  </span><span class="kd">def</span><span class="w"> </span><span class="nf">render</span><span class="p" data-group-id="0509570109-13">(</span><span class="n">assigns</span><span class="p" data-group-id="0509570109-13">)</span><span class="w"> </span><span class="k" data-group-id="0509570109-14">do</span><span class="w">
    </span><span class="sx">~X&quot;&quot;&quot;
    &lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&gt;
    &lt;feed xmlns=&quot;http://www.w3.org/2005/Atom&quot;&gt;
      &lt;title&gt;&lt;%= @title %&gt;&lt;/title&gt;
      &lt;link href=&quot;&lt;%= url(~p&quot;/&quot;) %&gt;&quot; /&gt;
      &lt;link href=&quot;&lt;%= url(~p&quot;/atom.xml&quot;) %&gt;&quot; rel=&quot;self&quot; /&gt;
      &lt;updated&gt;&lt;%= @updated_at %&gt;&lt;/updated&gt;
      &lt;author&gt;
        &lt;name&gt;&lt;%= @author %&gt;&lt;/name&gt;
      &lt;/author&gt;
      &lt;id&gt;&lt;%= url(~p&quot;/&quot;) %&gt;&lt;/id&gt;

      &lt;%= for post &lt;- @posts do %&gt;
      &lt;entry&gt;
        &lt;title&gt;&lt;%= post.title %&gt;&lt;/title&gt;
        &lt;link href=&quot;&lt;%= url(~p&quot;/posts/#{post.id}&quot;) %&gt;&quot; /&gt;
        &lt;id&gt;&lt;%= url(~p&quot;/posts/#{post.id}&quot;) %&gt;&lt;/id&gt;
        &lt;updated&gt;&lt;%= post.date %&gt;&lt;/updated&gt;
        &lt;summary&gt;&lt;%= post.description %&gt;&lt;/summary&gt;
      &lt;/entry&gt;
      &lt;% end %&gt;
    &lt;/feed&gt;
    &quot;&quot;&quot;</span><span class="w">
  </span><span class="k" data-group-id="0509570109-14">end</span><span class="w">
</span><span class="k" data-group-id="0509570109-1">end</span></code></pre>
<p>
Great, this looks very similar to Phoenix components!</p>
<p>
The module has a <code class="inline">render/1</code> function that contains the compiled template, and a <code class="inline">load/1</code> function that’ll build the <code class="inline">assigns</code> map. Now we must tie this together with compiling the template.</p>
<p>
First, we should add an <code class="inline">xml/0</code> function to our web module so we can render routes:</p>
<pre><code class="makeup elixir"><span class="c1"># lib/my_blog_web/my_blog_web.ex</span><span class="w">
</span><span class="kd">defmodule</span><span class="w"> </span><span class="nc">MyBlogWeb</span><span class="w"> </span><span class="k" data-group-id="1475370296-1">do</span><span class="w">
  </span><span class="c1"># ...</span><span class="w">

  </span><span class="kd">def</span><span class="w"> </span><span class="nf">xml</span><span class="w"> </span><span class="k" data-group-id="1475370296-2">do</span><span class="w">
    </span><span class="k">quote</span><span class="w"> </span><span class="k" data-group-id="1475370296-3">do</span><span class="w">
      </span><span class="na">@behaviour</span><span class="w"> </span><span class="nc">MyBlogWeb.XML.Engine</span><span class="w">

      </span><span class="kn">import</span><span class="w"> </span><span class="nc">MyBlogWeb.XML.Engine</span><span class="w">
      </span><span class="kn">import</span><span class="w"> </span><span class="nc">MyBlogWeb.Gettext</span><span class="w">

      </span><span class="c1"># Routes generation with the ~p sigil</span><span class="w">
      </span><span class="k">unquote</span><span class="p" data-group-id="1475370296-4">(</span><span class="n">verified_routes</span><span class="p" data-group-id="1475370296-5">(</span><span class="p" data-group-id="1475370296-5">)</span><span class="p" data-group-id="1475370296-4">)</span><span class="w">
    </span><span class="k" data-group-id="1475370296-3">end</span><span class="w">
  </span><span class="k" data-group-id="1475370296-2">end</span><span class="w">

  </span><span class="c1"># ...</span><span class="w">
</span><span class="k" data-group-id="1475370296-1">end</span></code></pre>
<p>
Now, we get to the template engine!</p>
<pre><code class="makeup elixir"><span class="c1"># lib/my_blog_web/xml/engine.ex</span><span class="w">
</span><span class="kd">defmodule</span><span class="w"> </span><span class="nc">MyBlogWeb.XML.Engine</span><span class="w"> </span><span class="k" data-group-id="5549442215-1">do</span><span class="w">
  </span><span class="na">@type</span><span class="w"> </span><span class="n">document</span><span class="w"> </span><span class="o">::</span><span class="w"> </span><span class="p" data-group-id="5549442215-2">[</span><span class="n">any</span><span class="p" data-group-id="5549442215-3">(</span><span class="p" data-group-id="5549442215-3">)</span><span class="p" data-group-id="5549442215-2">]</span><span class="w">
  </span><span class="na">@type</span><span class="w"> </span><span class="n">assigns</span><span class="w"> </span><span class="o">::</span><span class="w"> </span><span class="n">map</span><span class="p" data-group-id="5549442215-4">(</span><span class="p" data-group-id="5549442215-4">)</span><span class="w">

  </span><span class="na">@callback</span><span class="w"> </span><span class="n">load</span><span class="p" data-group-id="5549442215-5">(</span><span class="n">keyword</span><span class="p" data-group-id="5549442215-6">(</span><span class="p" data-group-id="5549442215-6">)</span><span class="p" data-group-id="5549442215-5">)</span><span class="w"> </span><span class="o">::</span><span class="w"> </span><span class="n">assigns</span><span class="p" data-group-id="5549442215-7">(</span><span class="p" data-group-id="5549442215-7">)</span><span class="w">
  </span><span class="na">@callback</span><span class="w"> </span><span class="n">render</span><span class="p" data-group-id="5549442215-8">(</span><span class="n">assigns</span><span class="p" data-group-id="5549442215-9">(</span><span class="p" data-group-id="5549442215-9">)</span><span class="p" data-group-id="5549442215-8">)</span><span class="w"> </span><span class="o">::</span><span class="w"> </span><span class="n">document</span><span class="p" data-group-id="5549442215-10">(</span><span class="p" data-group-id="5549442215-10">)</span><span class="w">

  </span><span class="kd">defmacro</span><span class="w"> </span><span class="nf">sigil_X</span><span class="p" data-group-id="5549442215-11">(</span><span class="p" data-group-id="5549442215-12">{</span><span class="ss">:&lt;&lt;&gt;&gt;</span><span class="p">,</span><span class="w"> </span><span class="n">meta</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="5549442215-13">[</span><span class="n">expr</span><span class="p" data-group-id="5549442215-13">]</span><span class="p" data-group-id="5549442215-12">}</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="5549442215-14">[</span><span class="p" data-group-id="5549442215-14">]</span><span class="p" data-group-id="5549442215-11">)</span><span class="w"> </span><span class="k" data-group-id="5549442215-15">do</span><span class="w">
    </span><span class="k">unless</span><span class="w"> </span><span class="nc">Macro.Env</span><span class="o">.</span><span class="n">has_var?</span><span class="p" data-group-id="5549442215-16">(</span><span class="bp">__CALLER__</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="5549442215-17">{</span><span class="ss">:assigns</span><span class="p">,</span><span class="w"> </span><span class="no">nil</span><span class="p" data-group-id="5549442215-17">}</span><span class="p" data-group-id="5549442215-16">)</span><span class="w"> </span><span class="k" data-group-id="5549442215-18">do</span><span class="w">
      </span><span class="k">raise</span><span class="w"> </span><span class="s">&quot;~H requires a variable named </span><span class="se">\&quot;</span><span class="s">assigns</span><span class="se">\&quot;</span><span class="s"> to exist and be set to a map&quot;</span><span class="w">
    </span><span class="k" data-group-id="5549442215-18">end</span><span class="w">

    </span><span class="n">options</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p" data-group-id="5549442215-19">[</span><span class="w">
      </span><span class="ss">engine</span><span class="p">:</span><span class="w"> </span><span class="bp">__MODULE__</span><span class="p">,</span><span class="w">
      </span><span class="ss">file</span><span class="p">:</span><span class="w"> </span><span class="bp">__CALLER__</span><span class="o">.</span><span class="n">file</span><span class="p">,</span><span class="w">
      </span><span class="ss">line</span><span class="p">:</span><span class="w"> </span><span class="bp">__CALLER__</span><span class="o">.</span><span class="n">line</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span><span class="w">
      </span><span class="ss">caller</span><span class="p">:</span><span class="w"> </span><span class="bp">__CALLER__</span><span class="p">,</span><span class="w">
      </span><span class="ss">indentation</span><span class="p">:</span><span class="w"> </span><span class="n">meta</span><span class="p" data-group-id="5549442215-20">[</span><span class="ss">:indentation</span><span class="p" data-group-id="5549442215-20">]</span><span class="w"> </span><span class="o">||</span><span class="w"> </span><span class="mi">0</span><span class="p">,</span><span class="w">
      </span><span class="ss">source</span><span class="p">:</span><span class="w"> </span><span class="n">expr</span><span class="w">
    </span><span class="p" data-group-id="5549442215-19">]</span><span class="w">

    </span><span class="nc">EEx</span><span class="o">.</span><span class="n">compile_string</span><span class="p" data-group-id="5549442215-21">(</span><span class="n">expr</span><span class="p">,</span><span class="w"> </span><span class="n">options</span><span class="p" data-group-id="5549442215-21">)</span><span class="w">
  </span><span class="k" data-group-id="5549442215-15">end</span><span class="w">

  </span><span class="na">@behaviour</span><span class="w"> </span><span class="nc">EEx.Engine</span><span class="w">

  </span><span class="na">@impl</span><span class="w"> </span><span class="no">true</span><span class="w">
  </span><span class="kd">def</span><span class="w"> </span><span class="nf">init</span><span class="p" data-group-id="5549442215-22">(</span><span class="c">_opts</span><span class="p" data-group-id="5549442215-22">)</span><span class="w"> </span><span class="k" data-group-id="5549442215-23">do</span><span class="w">
    </span><span class="p" data-group-id="5549442215-24">%{</span><span class="w">
      </span><span class="ss">iodata</span><span class="p">:</span><span class="w"> </span><span class="p" data-group-id="5549442215-25">[</span><span class="p" data-group-id="5549442215-25">]</span><span class="p">,</span><span class="w">
      </span><span class="ss">dynamic</span><span class="p">:</span><span class="w"> </span><span class="p" data-group-id="5549442215-26">[</span><span class="p" data-group-id="5549442215-26">]</span><span class="p">,</span><span class="w">
      </span><span class="ss">vars_count</span><span class="p">:</span><span class="w"> </span><span class="mi">0</span><span class="w">
    </span><span class="p" data-group-id="5549442215-24">}</span><span class="w">
  </span><span class="k" data-group-id="5549442215-23">end</span><span class="w">

  </span><span class="na">@impl</span><span class="w"> </span><span class="no">true</span><span class="w">
  </span><span class="kd">def</span><span class="w"> </span><span class="nf">handle_begin</span><span class="p" data-group-id="5549442215-27">(</span><span class="n">state</span><span class="p" data-group-id="5549442215-27">)</span><span class="w"> </span><span class="k" data-group-id="5549442215-28">do</span><span class="w">
    </span><span class="p" data-group-id="5549442215-29">%{</span><span class="n">state</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="ss">iodata</span><span class="p">:</span><span class="w"> </span><span class="p" data-group-id="5549442215-30">[</span><span class="p" data-group-id="5549442215-30">]</span><span class="p">,</span><span class="w"> </span><span class="ss">dynamic</span><span class="p">:</span><span class="w"> </span><span class="p" data-group-id="5549442215-31">[</span><span class="p" data-group-id="5549442215-31">]</span><span class="p" data-group-id="5549442215-29">}</span><span class="w">
  </span><span class="k" data-group-id="5549442215-28">end</span><span class="w">

  </span><span class="na">@impl</span><span class="w"> </span><span class="no">true</span><span class="w">
  </span><span class="kd">def</span><span class="w"> </span><span class="nf">handle_end</span><span class="p" data-group-id="5549442215-32">(</span><span class="n">quoted</span><span class="p" data-group-id="5549442215-32">)</span><span class="w"> </span><span class="k" data-group-id="5549442215-33">do</span><span class="w">
    </span><span class="n">handle_body</span><span class="p" data-group-id="5549442215-34">(</span><span class="n">quoted</span><span class="p" data-group-id="5549442215-34">)</span><span class="w">
  </span><span class="k" data-group-id="5549442215-33">end</span><span class="w">

  </span><span class="na">@impl</span><span class="w"> </span><span class="no">true</span><span class="w">
  </span><span class="kd">def</span><span class="w"> </span><span class="nf">handle_body</span><span class="p" data-group-id="5549442215-35">(</span><span class="n">state</span><span class="p" data-group-id="5549442215-35">)</span><span class="w"> </span><span class="k" data-group-id="5549442215-36">do</span><span class="w">
    </span><span class="p" data-group-id="5549442215-37">%{</span><span class="ss">iodata</span><span class="p">:</span><span class="w"> </span><span class="n">iodata</span><span class="p">,</span><span class="w"> </span><span class="ss">dynamic</span><span class="p">:</span><span class="w"> </span><span class="n">dynamic</span><span class="p" data-group-id="5549442215-37">}</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">state</span><span class="w">
    </span><span class="n">iodata</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">Enum</span><span class="o">.</span><span class="n">reverse</span><span class="p" data-group-id="5549442215-38">(</span><span class="n">iodata</span><span class="p" data-group-id="5549442215-38">)</span><span class="w">
    </span><span class="p" data-group-id="5549442215-39">{</span><span class="ss">:__block__</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="5549442215-40">[</span><span class="p" data-group-id="5549442215-40">]</span><span class="p">,</span><span class="w"> </span><span class="nc">Enum</span><span class="o">.</span><span class="n">reverse</span><span class="p" data-group-id="5549442215-41">(</span><span class="p" data-group-id="5549442215-42">[</span><span class="n">iodata</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">dynamic</span><span class="p" data-group-id="5549442215-42">]</span><span class="p" data-group-id="5549442215-41">)</span><span class="p" data-group-id="5549442215-39">}</span><span class="w">
  </span><span class="k" data-group-id="5549442215-36">end</span><span class="w">

  </span><span class="na">@impl</span><span class="w"> </span><span class="no">true</span><span class="w">
  </span><span class="kd">def</span><span class="w"> </span><span class="nf">handle_text</span><span class="p" data-group-id="5549442215-43">(</span><span class="n">state</span><span class="p">,</span><span class="w"> </span><span class="c">_meta</span><span class="p">,</span><span class="w"> </span><span class="n">text</span><span class="p" data-group-id="5549442215-43">)</span><span class="w"> </span><span class="k" data-group-id="5549442215-44">do</span><span class="w">
    </span><span class="p" data-group-id="5549442215-45">%{</span><span class="ss">iodata</span><span class="p">:</span><span class="w"> </span><span class="n">iodata</span><span class="p" data-group-id="5549442215-45">}</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">state</span><span class="w">
    </span><span class="p" data-group-id="5549442215-46">%{</span><span class="n">state</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="ss">iodata</span><span class="p">:</span><span class="w"> </span><span class="p" data-group-id="5549442215-47">[</span><span class="n">text</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">iodata</span><span class="p" data-group-id="5549442215-47">]</span><span class="p" data-group-id="5549442215-46">}</span><span class="w">
  </span><span class="k" data-group-id="5549442215-44">end</span><span class="w">

  </span><span class="na">@impl</span><span class="w"> </span><span class="no">true</span><span class="w">
  </span><span class="kd">def</span><span class="w"> </span><span class="nf">handle_expr</span><span class="p" data-group-id="5549442215-48">(</span><span class="n">state</span><span class="p">,</span><span class="w"> </span><span class="s">&quot;=&quot;</span><span class="p">,</span><span class="w"> </span><span class="n">ast</span><span class="p" data-group-id="5549442215-48">)</span><span class="w"> </span><span class="k" data-group-id="5549442215-49">do</span><span class="w">
    </span><span class="n">ast</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">Macro</span><span class="o">.</span><span class="n">prewalk</span><span class="p" data-group-id="5549442215-50">(</span><span class="n">ast</span><span class="p">,</span><span class="w"> </span><span class="o">&amp;</span><span class="nc">EEx.Engine</span><span class="o">.</span><span class="n">handle_assign</span><span class="o">/</span><span class="mi">1</span><span class="p" data-group-id="5549442215-50">)</span><span class="w">
    </span><span class="p" data-group-id="5549442215-51">%{</span><span class="ss">iodata</span><span class="p">:</span><span class="w"> </span><span class="n">iodata</span><span class="p">,</span><span class="w"> </span><span class="ss">dynamic</span><span class="p">:</span><span class="w"> </span><span class="n">dynamic</span><span class="p">,</span><span class="w"> </span><span class="ss">vars_count</span><span class="p">:</span><span class="w"> </span><span class="n">vars_count</span><span class="p" data-group-id="5549442215-51">}</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">state</span><span class="w">
    </span><span class="n">var</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">Macro</span><span class="o">.</span><span class="n">var</span><span class="p" data-group-id="5549442215-52">(</span><span class="ss">:&quot;arg</span><span class="si" data-group-id="5549442215-53">#{</span><span class="n">vars_count</span><span class="si" data-group-id="5549442215-53">}</span><span class="ss">&quot;</span><span class="p">,</span><span class="w"> </span><span class="bp">__MODULE__</span><span class="p" data-group-id="5549442215-52">)</span><span class="w">
    </span><span class="n">ast</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">quote</span><span class="w"> </span><span class="ss">do</span><span class="p">:</span><span class="w"> </span><span class="k">unquote</span><span class="p" data-group-id="5549442215-54">(</span><span class="n">var</span><span class="p" data-group-id="5549442215-54">)</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">unquote</span><span class="p" data-group-id="5549442215-55">(</span><span class="n">ast</span><span class="p" data-group-id="5549442215-55">)</span><span class="w">
    </span><span class="p" data-group-id="5549442215-56">%{</span><span class="n">state</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="ss">dynamic</span><span class="p">:</span><span class="w"> </span><span class="p" data-group-id="5549442215-57">[</span><span class="n">ast</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">dynamic</span><span class="p" data-group-id="5549442215-57">]</span><span class="p">,</span><span class="w"> </span><span class="ss">iodata</span><span class="p">:</span><span class="w"> </span><span class="p" data-group-id="5549442215-58">[</span><span class="n">var</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">iodata</span><span class="p" data-group-id="5549442215-58">]</span><span class="p">,</span><span class="w"> </span><span class="ss">vars_count</span><span class="p">:</span><span class="w"> </span><span class="n">vars_count</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="mi">1</span><span class="p" data-group-id="5549442215-56">}</span><span class="w">
  </span><span class="k" data-group-id="5549442215-49">end</span><span class="w">

  </span><span class="kd">def</span><span class="w"> </span><span class="nf">handle_expr</span><span class="p" data-group-id="5549442215-59">(</span><span class="n">state</span><span class="p">,</span><span class="w"> </span><span class="s">&quot;&quot;</span><span class="p">,</span><span class="w"> </span><span class="n">ast</span><span class="p" data-group-id="5549442215-59">)</span><span class="w"> </span><span class="k" data-group-id="5549442215-60">do</span><span class="w">
    </span><span class="n">ast</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">Macro</span><span class="o">.</span><span class="n">prewalk</span><span class="p" data-group-id="5549442215-61">(</span><span class="n">ast</span><span class="p">,</span><span class="w"> </span><span class="o">&amp;</span><span class="nc">EEx.Engine</span><span class="o">.</span><span class="n">handle_assign</span><span class="o">/</span><span class="mi">1</span><span class="p" data-group-id="5549442215-61">)</span><span class="w">
    </span><span class="p" data-group-id="5549442215-62">%{</span><span class="ss">dynamic</span><span class="p">:</span><span class="w"> </span><span class="n">dynamic</span><span class="p" data-group-id="5549442215-62">}</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">state</span><span class="w">
    </span><span class="p" data-group-id="5549442215-63">%{</span><span class="n">state</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="ss">dynamic</span><span class="p">:</span><span class="w"> </span><span class="p" data-group-id="5549442215-64">[</span><span class="n">ast</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">dynamic</span><span class="p" data-group-id="5549442215-64">]</span><span class="p" data-group-id="5549442215-63">}</span><span class="w">
  </span><span class="k" data-group-id="5549442215-60">end</span><span class="w">

  </span><span class="kd">def</span><span class="w"> </span><span class="nf">handle_expr</span><span class="p" data-group-id="5549442215-65">(</span><span class="n">state</span><span class="p">,</span><span class="w"> </span><span class="n">marker</span><span class="p">,</span><span class="w"> </span><span class="n">ast</span><span class="p" data-group-id="5549442215-65">)</span><span class="w"> </span><span class="k" data-group-id="5549442215-66">do</span><span class="w">
    </span><span class="nc">EEx.Engine</span><span class="o">.</span><span class="n">handle_expr</span><span class="p" data-group-id="5549442215-67">(</span><span class="n">state</span><span class="p">,</span><span class="w"> </span><span class="n">marker</span><span class="p">,</span><span class="w"> </span><span class="n">ast</span><span class="p" data-group-id="5549442215-67">)</span><span class="w">
  </span><span class="k" data-group-id="5549442215-66">end</span><span class="w">

  </span><span class="kd">def</span><span class="w"> </span><span class="nf">encode_to_iodata!</span><span class="p" data-group-id="5549442215-68">(</span><span class="n">data</span><span class="p" data-group-id="5549442215-68">)</span><span class="p">,</span><span class="w"> </span><span class="ss">do</span><span class="p">:</span><span class="w"> </span><span class="n">to_iodata</span><span class="p" data-group-id="5549442215-69">(</span><span class="n">data</span><span class="p" data-group-id="5549442215-69">)</span><span class="w">

  </span><span class="kd">defp</span><span class="w"> </span><span class="nf">to_iodata</span><span class="p" data-group-id="5549442215-70">(</span><span class="p" data-group-id="5549442215-71">[</span><span class="n">h</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">t</span><span class="p" data-group-id="5549442215-71">]</span><span class="p" data-group-id="5549442215-70">)</span><span class="p">,</span><span class="w"> </span><span class="ss">do</span><span class="p">:</span><span class="w"> </span><span class="p" data-group-id="5549442215-72">[</span><span class="n">to_iodata</span><span class="p" data-group-id="5549442215-73">(</span><span class="n">h</span><span class="p" data-group-id="5549442215-73">)</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">to_iodata</span><span class="p" data-group-id="5549442215-74">(</span><span class="n">t</span><span class="p" data-group-id="5549442215-74">)</span><span class="p" data-group-id="5549442215-72">]</span><span class="w">
  </span><span class="kd">defp</span><span class="w"> </span><span class="nf">to_iodata</span><span class="p" data-group-id="5549442215-75">(</span><span class="n">text</span><span class="p" data-group-id="5549442215-75">)</span><span class="w"> </span><span class="ow">when</span><span class="w"> </span><span class="n">is_binary</span><span class="p" data-group-id="5549442215-76">(</span><span class="n">text</span><span class="p" data-group-id="5549442215-76">)</span><span class="p">,</span><span class="w"> </span><span class="ss">do</span><span class="p">:</span><span class="w"> </span><span class="n">text</span><span class="w">
  </span><span class="kd">defp</span><span class="w"> </span><span class="nf">to_iodata</span><span class="p" data-group-id="5549442215-77">(</span><span class="p" data-group-id="5549442215-78">%</span><span class="nc" data-group-id="5549442215-78">DateTime</span><span class="p" data-group-id="5549442215-78">{</span><span class="p" data-group-id="5549442215-78">}</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">date</span><span class="p" data-group-id="5549442215-77">)</span><span class="p">,</span><span class="w"> </span><span class="ss">do</span><span class="p">:</span><span class="w"> </span><span class="nc">DateTime</span><span class="o">.</span><span class="n">to_iso8601</span><span class="p" data-group-id="5549442215-79">(</span><span class="n">date</span><span class="p" data-group-id="5549442215-79">)</span><span class="w">
  </span><span class="kd">defp</span><span class="w"> </span><span class="nf">to_iodata</span><span class="p" data-group-id="5549442215-80">(</span><span class="p" data-group-id="5549442215-81">[</span><span class="p" data-group-id="5549442215-81">]</span><span class="p" data-group-id="5549442215-80">)</span><span class="p">,</span><span class="w"> </span><span class="ss">do</span><span class="p">:</span><span class="w"> </span><span class="s">&quot;&quot;</span><span class="w">
</span><span class="k" data-group-id="5549442215-1">end</span></code></pre>
<p>
The template engine will transform the template string defined in <code class="inline">MyBlogWeb.AtomXML</code> into an AST. The above logic is similar to how the <a href="https://github.com/phoenixframework/phoenix_html/blob/v3.3.0/lib/phoenix_html/engine.ex"><code class="inline">Phoenix.HTML.Engine</code></a> works.</p>
<p>
I did want to add XML document validation here as well, but that was a daunting task when looking at how <a href="https://github.com/phoenixframework/phoenix_live_view/blob/v0.18.15/lib/phoenix_live_view/html_engine.ex"><code class="inline">Phoenix.LiveView.HTMLEngine</code></a> deals with it. This is good enough as we still get a compile-time check of the embedded Elixir code!</p>
<p>
Now that we can generate our XML document, we need to serve it as a file in Phoenix.</p>
<h2>
Option 1: Assets deploy pipeline</h2>
<p>
It makes sense that <code class="inline">atom.xml</code> only needs to be generated at compile time since all our blog posts are static. So we could add <code class="inline">atom.xml</code> file generation to the assets deploy pipeline.</p>
<p>
As you will see below it feels messier than just generating it in a plug, due to how code-generated Phoenix thinks of URL generator options as runtime-only config.</p>
<p>
First, we’ll add a mix alias to write to <code class="inline">priv/static/atom.xml</code>:</p>
<pre><code class="makeup elixir"><span class="c1"># mix.exs</span><span class="w">
</span><span class="kd">defmodule</span><span class="w"> </span><span class="nc">MyBlog.MixProject</span><span class="w"> </span><span class="k" data-group-id="8681742489-1">do</span><span class="w">
  </span><span class="c1"># ...</span><span class="w">

  </span><span class="kd">defp</span><span class="w"> </span><span class="nf">aliases</span><span class="w"> </span><span class="k" data-group-id="8681742489-2">do</span><span class="w">
    </span><span class="p" data-group-id="8681742489-3">[</span><span class="w">
      </span><span class="c1"># ...</span><span class="w">
      </span><span class="ss">&quot;assets.gen.atom&quot;</span><span class="p">:</span><span class="w"> </span><span class="o">&amp;</span><span class="n">write_atom_feed</span><span class="o">/</span><span class="mi">1</span><span class="p">,</span><span class="w">
      </span><span class="ss">&quot;assets.deploy&quot;</span><span class="p">:</span><span class="w"> </span><span class="p" data-group-id="8681742489-4">[</span><span class="s">&quot;tailwind default --minify&quot;</span><span class="p">,</span><span class="w"> </span><span class="s">&quot;esbuild default --minify&quot;</span><span class="p">,</span><span class="w"> </span><span class="s">&quot;phx.digest&quot;</span><span class="p">,</span><span class="w"> </span><span class="s">&quot;assets.gen.atom&quot;</span><span class="p" data-group-id="8681742489-4">]</span><span class="w">
    </span><span class="p" data-group-id="8681742489-3">]</span><span class="w">
  </span><span class="k" data-group-id="8681742489-2">end</span><span class="w">

  </span><span class="na">@atom_feed_path</span><span class="w"> </span><span class="s">&quot;priv/static/atom.xml&quot;</span><span class="w">

  </span><span class="kd">defp</span><span class="w"> </span><span class="nf">write_atom_feed</span><span class="p" data-group-id="8681742489-5">(</span><span class="n">args</span><span class="p" data-group-id="8681742489-5">)</span><span class="w"> </span><span class="k" data-group-id="8681742489-6">do</span><span class="w">
    </span><span class="nc">Mix.Task</span><span class="o">.</span><span class="n">run</span><span class="p" data-group-id="8681742489-7">(</span><span class="s">&quot;app.start&quot;</span><span class="p">,</span><span class="w"> </span><span class="n">args</span><span class="p" data-group-id="8681742489-7">)</span><span class="w">

    </span><span class="n">content</span><span class="w"> </span><span class="o">=</span><span class="w">
      </span><span class="p" data-group-id="8681742489-8">[</span><span class="p" data-group-id="8681742489-8">]</span><span class="w">
      </span><span class="o">|&gt;</span><span class="w"> </span><span class="nc">MyBlogWeb.AtomXML</span><span class="o">.</span><span class="n">load</span><span class="p" data-group-id="8681742489-9">(</span><span class="p" data-group-id="8681742489-9">)</span><span class="w">
      </span><span class="o">|&gt;</span><span class="w"> </span><span class="nc">MyBlogWeb.AtomXML</span><span class="o">.</span><span class="n">render</span><span class="p" data-group-id="8681742489-10">(</span><span class="p" data-group-id="8681742489-10">)</span><span class="w">
      </span><span class="o">|&gt;</span><span class="w"> </span><span class="nc">MyBlogWeb.XML.Engine</span><span class="o">.</span><span class="n">encode_to_iodata!</span><span class="p" data-group-id="8681742489-11">(</span><span class="p" data-group-id="8681742489-11">)</span><span class="w">
      </span><span class="o">|&gt;</span><span class="w"> </span><span class="nc">IO</span><span class="o">.</span><span class="n">iodata_to_binary</span><span class="p" data-group-id="8681742489-12">(</span><span class="p" data-group-id="8681742489-12">)</span><span class="w">

    </span><span class="k">case</span><span class="w"> </span><span class="nc">File</span><span class="o">.</span><span class="n">write</span><span class="p" data-group-id="8681742489-13">(</span><span class="na">@atom_feed_path</span><span class="p">,</span><span class="w"> </span><span class="n">content</span><span class="p" data-group-id="8681742489-13">)</span><span class="w"> </span><span class="k" data-group-id="8681742489-14">do</span><span class="w">
      </span><span class="ss">:ok</span><span class="w"> </span><span class="o">-&gt;</span><span class="w"> </span><span class="nc">Mix</span><span class="o">.</span><span class="n">shell</span><span class="p" data-group-id="8681742489-15">(</span><span class="p" data-group-id="8681742489-15">)</span><span class="o">.</span><span class="n">info</span><span class="p" data-group-id="8681742489-16">(</span><span class="p" data-group-id="8681742489-17">[</span><span class="ss">:green</span><span class="p">,</span><span class="w"> </span><span class="s">&quot;Generated file </span><span class="si" data-group-id="8681742489-18">#{</span><span class="na">@atom_feed_path</span><span class="si" data-group-id="8681742489-18">}</span><span class="s">&quot;</span><span class="p" data-group-id="8681742489-17">]</span><span class="p" data-group-id="8681742489-16">)</span><span class="w">
      </span><span class="p" data-group-id="8681742489-19">{</span><span class="ss">:error</span><span class="p">,</span><span class="w"> </span><span class="n">posix</span><span class="p" data-group-id="8681742489-19">}</span><span class="w"> </span><span class="o">-&gt;</span><span class="w"> </span><span class="nc">Mix</span><span class="o">.</span><span class="n">shell</span><span class="p" data-group-id="8681742489-20">(</span><span class="p" data-group-id="8681742489-20">)</span><span class="o">.</span><span class="n">info</span><span class="p" data-group-id="8681742489-21">(</span><span class="p" data-group-id="8681742489-22">[</span><span class="ss">:red</span><span class="p">,</span><span class="w"> </span><span class="s">&quot;Failed to write to </span><span class="si" data-group-id="8681742489-23">#{</span><span class="na">@atom_feed_path</span><span class="si" data-group-id="8681742489-23">}</span><span class="s">: </span><span class="si" data-group-id="8681742489-24">#{</span><span class="n">inspect</span><span class="w"> </span><span class="n">posix</span><span class="si" data-group-id="8681742489-24">}</span><span class="s">&quot;</span><span class="p" data-group-id="8681742489-22">]</span><span class="p" data-group-id="8681742489-21">)</span><span class="w">
    </span><span class="k" data-group-id="8681742489-14">end</span><span class="w">
  </span><span class="k" data-group-id="8681742489-6">end</span><span class="w">
</span><span class="k" data-group-id="8681742489-1">end</span></code></pre>
<p>
Now you can run <code class="inline">mix assets.gen.atom</code> to generate <code class="inline">priv/static/atom.xml</code>!</p>
<p>
We add this alias to the <code class="inline">assets.deploy</code> alias <em>after</em> <code class="inline">phx.digest</code> because we need the URI to stay consistent for all deployments. It would break feed reader sync if the URI got an ever-changing cache-busting fingerprint.</p>
<p>
To serve the file as static content in Phoenix, add <code class="inline">atom.xml</code> to <code class="inline">statics_path/0</code> in the web module:</p>
<pre><code class="makeup elixir"><span class="c1"># lib/my_blog_web/my_blog_web.ex</span><span class="w">
</span><span class="kd">defmodule</span><span class="w"> </span><span class="nc">MyBlogWeb</span><span class="w"> </span><span class="k" data-group-id="0075421661-1">do</span><span class="w">

  </span><span class="c1"># ...</span><span class="w">
  
  </span><span class="kd">def</span><span class="w"> </span><span class="nf">static_paths</span><span class="p">,</span><span class="w"> </span><span class="ss">do</span><span class="p">:</span><span class="w"> </span><span class="sx">~w(assets fonts images favicon.ico robots.txt atom.xml)</span><span class="w">
  
  </span><span class="c1"># ...</span><span class="w">
</span><span class="k" data-group-id="0075421661-1">end</span></code></pre>
<p>
Now add the Atom feed link to <code class="inline">&lt;head&gt;</code> in our root template:</p>
<pre><code class="makeup elixir"><span class="c1"># lib/my_blog_web/components/layouts/root.html.heex</span><span class="w">
</span><span class="o">&lt;</span><span class="n">link</span><span class="w"> </span><span class="n">href</span><span class="o">=</span><span class="p" data-group-id="5582790052-1">{</span><span class="sx">~p&quot;/atom.xml&quot;</span><span class="p" data-group-id="5582790052-1">}</span><span class="w"> </span><span class="n">type</span><span class="o">=</span><span class="s">&quot;application/atom+xml&quot;</span><span class="w"> </span><span class="n">rel</span><span class="o">=</span><span class="s">&quot;alternate&quot;</span><span class="w"> </span><span class="n">title</span><span class="o">=</span><span class="s">&quot;Atom feed&quot;</span><span class="w"> </span><span class="o">/</span><span class="o">&gt;</span></code></pre>
<p>
As the feed is autogenerated you may also want to ignore it in <code class="inline">.gitignore</code> so it doesn’t show up in the working tree:</p>
<pre><code class="bash"># .gitignore

# Ignore assets that are produced by build tools.
/priv/static/atom.xml</code></pre>
<p>
When we build the docker image, we need to make sure that absolute URIs in our <code class="inline">atom.xml</code> are correct. In the code-generated Phoenix app, the URL options are set in <code class="inline">config/runtime.exs</code> with a <code class="inline">PHX_HOST</code> env var. Naturally, the config is not included when the code-generated Phoenix Dockerfile compiles the app.</p>
<p>
So to have the URL options available at compile-time we first need to move the URL generation options over to <code class="inline">config/prod.exs</code>:</p>
<pre><code class="makeup elixir"><span class="c1"># config/prod.exs</span><span class="w">

</span><span class="n">config</span><span class="w"> </span><span class="ss">:dan</span><span class="p">,</span><span class="w"> </span><span class="nc">DanWeb.Endpoint</span><span class="p">,</span><span class="w">
  </span><span class="ss">cache_static_manifest</span><span class="p">:</span><span class="w"> </span><span class="s">&quot;priv/static/cache_manifest.json&quot;</span><span class="p">,</span><span class="w">
  </span><span class="ss">url</span><span class="p">:</span><span class="w"> </span><span class="p" data-group-id="6161713998-1">[</span><span class="ss">host</span><span class="p">:</span><span class="w"> </span><span class="nc">System</span><span class="o">.</span><span class="n">fetch_env!</span><span class="p" data-group-id="6161713998-2">(</span><span class="s">&quot;PHX_HOST&quot;</span><span class="p" data-group-id="6161713998-2">)</span><span class="p">,</span><span class="w"> </span><span class="ss">port</span><span class="p">:</span><span class="w"> </span><span class="mi">443</span><span class="p">,</span><span class="w"> </span><span class="ss">scheme</span><span class="p">:</span><span class="w"> </span><span class="s">&quot;https&quot;</span><span class="p" data-group-id="6161713998-1">]</span></code></pre>
<p>
Then we’ll need to make the <code class="inline">PHX_HOST</code> env var available in the Dockerfile before calling <code class="inline">mix deps.compile</code>:</p>
<pre><code class="Dockerfile"># Dockerfile

# ...

# set PHX_HOST ENV
ARG PHX_HOST
ENV PHX_HOST $PHX_HOST

# ...</code></pre>
<p>
It’s all working now! But this doesn’t feel quite right. First, we had to prevent the cache-busting fingerprint, and then, ensure that we can generate absolute URIs at compile-time. Maybe it’s more Phoenix native to only generate absolute URIs at run-time?</p>
<h2>
Option 2: Plug</h2>
<p>
First, we’ll create a macro that sets up <code class="inline">MyBlogWeb.AtomXML</code> to be a plug:</p>
<pre><code class="makeup elixir"><span class="c1"># lib/my_blog_web/xml/plug.ex</span><span class="w">
</span><span class="kd">defmodule</span><span class="w"> </span><span class="nc">MyBlogWeb.XML.Plug</span><span class="w"> </span><span class="k" data-group-id="4285204857-1">do</span><span class="w">
  </span><span class="kd">defmacro</span><span class="w"> </span><span class="nf">__using__</span><span class="p" data-group-id="4285204857-2">(</span><span class="bp">_</span><span class="p" data-group-id="4285204857-2">)</span><span class="w"> </span><span class="k" data-group-id="4285204857-3">do</span><span class="w">
    </span><span class="k">quote</span><span class="w"> </span><span class="k" data-group-id="4285204857-4">do</span><span class="w">
      </span><span class="na">@behaviour</span><span class="w"> </span><span class="nc">Plug</span><span class="w">

      </span><span class="kn">import</span><span class="w"> </span><span class="nc">Plug.Conn</span><span class="w">

      </span><span class="na">@doc</span><span class="w"> </span><span class="no">false</span><span class="w">
      </span><span class="kd">def</span><span class="w"> </span><span class="nf">init</span><span class="p" data-group-id="4285204857-5">(</span><span class="n">opts</span><span class="p" data-group-id="4285204857-5">)</span><span class="p">,</span><span class="w"> </span><span class="ss">do</span><span class="p">:</span><span class="w"> </span><span class="n">opts</span><span class="w">

      </span><span class="na">@doc</span><span class="w"> </span><span class="no">false</span><span class="w">
      </span><span class="kd">def</span><span class="w"> </span><span class="nf">call</span><span class="p" data-group-id="4285204857-6">(</span><span class="n">conn</span><span class="p">,</span><span class="w"> </span><span class="n">opts</span><span class="p" data-group-id="4285204857-6">)</span><span class="w"> </span><span class="k" data-group-id="4285204857-7">do</span><span class="w">
        </span><span class="n">data</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">__MODULE__</span><span class="o">.</span><span class="n">render</span><span class="p" data-group-id="4285204857-8">(</span><span class="bp">__MODULE__</span><span class="o">.</span><span class="n">load</span><span class="p" data-group-id="4285204857-9">(</span><span class="n">opts</span><span class="p" data-group-id="4285204857-9">)</span><span class="p" data-group-id="4285204857-8">)</span><span class="w">


        </span><span class="n">conn</span><span class="w">
        </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">put_resp_header</span><span class="p" data-group-id="4285204857-10">(</span><span class="s">&quot;content-type&quot;</span><span class="p">,</span><span class="w"> </span><span class="s">&quot;text/xml&quot;</span><span class="p" data-group-id="4285204857-10">)</span><span class="w">
        </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">resp</span><span class="p" data-group-id="4285204857-11">(</span><span class="mi">200</span><span class="p">,</span><span class="w"> </span><span class="nc">MyBlogWeb.XML.Engine</span><span class="o">.</span><span class="n">encode_to_iodata!</span><span class="p" data-group-id="4285204857-12">(</span><span class="n">data</span><span class="p" data-group-id="4285204857-12">)</span><span class="p" data-group-id="4285204857-11">)</span><span class="w">
      </span><span class="k" data-group-id="4285204857-7">end</span><span class="w">
    </span><span class="k" data-group-id="4285204857-4">end</span><span class="w">
  </span><span class="k" data-group-id="4285204857-3">end</span><span class="w">
</span><span class="k" data-group-id="4285204857-1">end</span></code></pre>
<p>
Now add <code class="inline">use MyBlogWeb.XML.Plug</code> to the <code class="inline">xml/0</code> function in the web module:</p>
<pre><code class="makeup elixir"><span class="c1"># lib/my_blog_web/my_blog_web.ex</span><span class="w">
</span><span class="kd">defmodule</span><span class="w"> </span><span class="nc">MyBlogWeb</span><span class="w"> </span><span class="k" data-group-id="9758475565-1">do</span><span class="w">
  </span><span class="c1"># ...</span><span class="w">

  </span><span class="kd">def</span><span class="w"> </span><span class="nf">xml</span><span class="w"> </span><span class="k" data-group-id="9758475565-2">do</span><span class="w">
    </span><span class="k">quote</span><span class="w"> </span><span class="k" data-group-id="9758475565-3">do</span><span class="w">
      </span><span class="na">@behaviour</span><span class="w"> </span><span class="nc">MyBlogWeb.XML.Engine</span><span class="w">

      </span><span class="kn">use</span><span class="w"> </span><span class="nc">MyBlogWeb.XML.Plug</span><span class="w">

      </span><span class="kn">import</span><span class="w"> </span><span class="nc">MyBlogWeb.XML.Engine</span><span class="w">
      </span><span class="kn">import</span><span class="w"> </span><span class="nc">MyBlogWeb.Gettext</span><span class="w">

      </span><span class="c1"># Routes generation with the ~p sigil</span><span class="w">
      </span><span class="k">unquote</span><span class="p" data-group-id="9758475565-4">(</span><span class="n">verified_routes</span><span class="p" data-group-id="9758475565-5">(</span><span class="p" data-group-id="9758475565-5">)</span><span class="p" data-group-id="9758475565-4">)</span><span class="w">
    </span><span class="k" data-group-id="9758475565-3">end</span><span class="w">
  </span><span class="k" data-group-id="9758475565-2">end</span><span class="w">

  </span><span class="c1"># ...</span><span class="w">
</span><span class="k" data-group-id="9758475565-1">end</span></code></pre>
<p>
Update the router to forward requests to the plug:</p>
<pre><code class="makeup elixir"><span class="c1"># lib/my_blog_web/router.ex</span><span class="w">
</span><span class="kd">defmodule</span><span class="w"> </span><span class="nc">MyBlogWeb.Router</span><span class="w"> </span><span class="k" data-group-id="1092475936-1">do</span><span class="w">
  </span><span class="kn">use</span><span class="w"> </span><span class="nc">MyBlogWeb</span><span class="p">,</span><span class="w"> </span><span class="ss">:router</span><span class="w">

  </span><span class="c1"># ...</span><span class="w">

  </span><span class="n">forward</span><span class="w"> </span><span class="s">&quot;/atom.xml&quot;</span><span class="p">,</span><span class="w"> </span><span class="nc">MyBlogWeb.AtomXML</span><span class="w">

  </span><span class="c1"># ...</span><span class="w">
</span><span class="k" data-group-id="1092475936-1">end</span></code></pre>
<p>
Finally, make sure to link it in <code class="inline">&lt;head&gt;</code>:</p>
<pre><code class="makeup elixir"><span class="c1"># lib/my_blog_web/components/layouts/root.html.heex</span><span class="w">
</span><span class="o">&lt;</span><span class="n">link</span><span class="w"> </span><span class="n">href</span><span class="o">=</span><span class="p" data-group-id="0440968444-1">{</span><span class="sx">~p&quot;/atom.xml&quot;</span><span class="p" data-group-id="0440968444-1">}</span><span class="w"> </span><span class="n">type</span><span class="o">=</span><span class="s">&quot;application/atom+xml&quot;</span><span class="w"> </span><span class="n">rel</span><span class="o">=</span><span class="s">&quot;alternate&quot;</span><span class="w"> </span><span class="n">title</span><span class="o">=</span><span class="s">&quot;Atom feed&quot;</span><span class="w"> </span><span class="o">/</span><span class="o">&gt;</span></code></pre>
<p>
Start the web server with <code class="inline">mix phx.server</code>, and you’ll see the feed generated at <a href="http://localhost:4000/atom.xml">http://localhost:4000/atom.xml</a>.</p>
<p>
Nice, this option feels a lot more in line with how Phoenix works by default!</p>

      ]]>
    </content>
  </entry>
  
  <entry>
    <title>IPv6-only network in Elixir</title>
    <link href="https://danschultzer.com/posts/ipv6-only-network-in-elixir" />
    <id>https://danschultzer.com/posts/ipv6-only-network-in-elixir</id>
    <updated>2023-02-14T00:00:00Z</updated>
    <summary>How I dealt with issues arising from setting up an Elixir app in an IPv6-only network.</summary>
    <content type="html">
      <![CDATA[
        <p>
I recently had to deal with deployment to AWS. I don’t know why it has to be like this, but AWS belongs in its own category of hell. The amount of effort it took to get to a production-ready infrastructure for Elixir running was mind-boggling. From my blood, sweat, and tears, I’ve <a href="https://github.com/danschultzer/elixir-terraform-aws-ecs-example">shared a repo</a> to help others save weeks or months of their lives dealing with this.</p>
<h2>
The IPv6-only story</h2>
<p>
One aspect of this infrastructure is IPv6-only networks. Our instances use an IPv6 address to communicate with the outside world. This eliminates the need for a NAT. However, the world still deals with IPv4, and IPv6 is not compatible with IPv4.</p>
<p>
So due to backward compatibility requirements, we’re living in a world of <a href="https://www.juniper.net/documentation/us/en/software/junos/is-is/topics/concept/ipv6-dual-stack-understanding.html">dual-stack IPv4/IPv6</a>.</p>
<p>
Dual-stack works by having both an IPv4 and IPv6 address available. Programming will decide what address to use to connect to a hostname. <a href="https://www.rfc-editor.org/rfc/rfc8305">Happy Eyeballs algorithm</a> will connect to both addresses in parallel, and whichever is the fastest to respond will be the one used.</p>
<p>
In OTP/Elixir we don’t have any built-in algorithm deciding what to connect with. Neither is there a way to globally configure what IP format to use. Instead, we have to explicitly set an <code class="inline">:inet6</code> flag when resolving the address:</p>
<pre><code class="makeup elixir"><span class="gp unselectable">iex(1)&gt; </span><span class="nc">:inet</span><span class="o">.</span><span class="n">getaddr</span><span class="p" data-group-id="9654425951-1">(</span><span class="sc">&#39;google.com&#39;</span><span class="p">,</span><span class="w"> </span><span class="ss">:inet</span><span class="p" data-group-id="9654425951-1">)</span><span class="w"> 
</span><span class="p" data-group-id="9654425951-2">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="9654425951-3">{</span><span class="mi">142</span><span class="p">,</span><span class="w"> </span><span class="mi">251</span><span class="p">,</span><span class="w"> </span><span class="mi">34</span><span class="p">,</span><span class="w"> </span><span class="mi">46</span><span class="p" data-group-id="9654425951-3">}</span><span class="p" data-group-id="9654425951-2">}</span><span class="w">

</span><span class="gp unselectable">iex(2)&gt; </span><span class="nc">:inet</span><span class="o">.</span><span class="n">getaddr</span><span class="p" data-group-id="9654425951-4">(</span><span class="sc">&#39;google.com&#39;</span><span class="p">,</span><span class="w"> </span><span class="ss">:inet6</span><span class="p" data-group-id="9654425951-4">)</span><span class="w">      
</span><span class="p" data-group-id="9654425951-5">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="9654425951-6">{</span><span class="mi">9735</span><span class="p">,</span><span class="w"> </span><span class="mi">63664</span><span class="p">,</span><span class="w"> </span><span class="mi">16391</span><span class="p">,</span><span class="w"> </span><span class="mi">2067</span><span class="p">,</span><span class="w"> </span><span class="mi">0</span><span class="p">,</span><span class="w"> </span><span class="mi">0</span><span class="p">,</span><span class="w"> </span><span class="mi">0</span><span class="p">,</span><span class="w"> </span><span class="mi">8206</span><span class="p" data-group-id="9654425951-6">}</span><span class="p" data-group-id="9654425951-5">}</span></code></pre>
<p>
This means that every single library that resolves a hostname to an address will have its particular way of resolving it.</p>
<h2>
<code class="inline">Honeybadger</code> and <code class="inline">:hackney</code></h2>
<p>
This became a problem with <code class="inline">Honeybadger</code>. Under the hood, <code class="inline">Honeybadger</code> uses <code class="inline">:hackney</code>. <code class="inline">:hackney</code> resolves the address <a href="https://github.com/benoitc/hackney/blob/6e79b2bb11a77389d3ba9ff3a0828a45796fe7a8/src/hackney_util.erl#L81-L91">like this</a>:</p>
<pre><code class="makeup erlang"><span class="nf">is_ipv6</span><span class="p" data-group-id="1726594679-1">(</span><span class="n">Host</span><span class="p" data-group-id="1726594679-1">)</span><span class="w"> </span><span class="p">-&gt;</span><span class="w">
  </span><span class="k">case</span><span class="w"> </span><span class="nc">inet_parse</span><span class="p">:</span><span class="nf">address</span><span class="p" data-group-id="1726594679-2">(</span><span class="n">Host</span><span class="p" data-group-id="1726594679-2">)</span><span class="w"> </span><span class="k">of</span><span class="w">
    </span><span class="p" data-group-id="1726594679-3">{</span><span class="ss">ok</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="1726594679-4">{</span><span class="p">_</span><span class="p">,</span><span class="w"> </span><span class="p">_</span><span class="p">,</span><span class="w"> </span><span class="p">_</span><span class="p">,</span><span class="w"> </span><span class="p">_</span><span class="p">,</span><span class="w"> </span><span class="p">_</span><span class="p">,</span><span class="w"> </span><span class="p">_</span><span class="p">,</span><span class="w"> </span><span class="p">_</span><span class="p">,</span><span class="w"> </span><span class="p">_</span><span class="p" data-group-id="1726594679-4">}</span><span class="p" data-group-id="1726594679-3">}</span><span class="w"> </span><span class="p">-&gt;</span><span class="w">
      </span><span class="ss">true</span><span class="p">;</span><span class="w">
    </span><span class="p" data-group-id="1726594679-5">{</span><span class="ss">ok</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="1726594679-6">{</span><span class="p">_</span><span class="p">,</span><span class="w"> </span><span class="p">_</span><span class="p">,</span><span class="w"> </span><span class="p">_</span><span class="p">,</span><span class="w"> </span><span class="p">_</span><span class="p" data-group-id="1726594679-6">}</span><span class="p" data-group-id="1726594679-5">}</span><span class="w"> </span><span class="p">-&gt;</span><span class="w">
      </span><span class="ss">false</span><span class="p">;</span><span class="w">
    </span><span class="p">_</span><span class="w"> </span><span class="p">-&gt;</span><span class="w">
      </span><span class="k">case</span><span class="w"> </span><span class="nc">inet</span><span class="p">:</span><span class="nf">getaddr</span><span class="p" data-group-id="1726594679-7">(</span><span class="n">Host</span><span class="p">,</span><span class="w"> </span><span class="ss">inet</span><span class="p" data-group-id="1726594679-7">)</span><span class="w"> </span><span class="k">of</span><span class="w">
        </span><span class="p" data-group-id="1726594679-8">{</span><span class="ss">ok</span><span class="p">,</span><span class="w"> </span><span class="p">_</span><span class="p" data-group-id="1726594679-8">}</span><span class="w"> </span><span class="p">-&gt;</span><span class="w">
          </span><span class="ss">false</span><span class="p">;</span><span class="w">
        </span><span class="p">_</span><span class="w"> </span><span class="p">-&gt;</span><span class="w">
          </span><span class="k">case</span><span class="w"> </span><span class="nc">inet</span><span class="p">:</span><span class="nf">getaddr</span><span class="p" data-group-id="1726594679-9">(</span><span class="n">Host</span><span class="p">,</span><span class="w"> </span><span class="ss">inet6</span><span class="p" data-group-id="1726594679-9">)</span><span class="w"> </span><span class="k">of</span><span class="w">
            </span><span class="p" data-group-id="1726594679-10">{</span><span class="ss">ok</span><span class="p">,</span><span class="w"> </span><span class="p">_</span><span class="p" data-group-id="1726594679-10">}</span><span class="w"> </span><span class="p">-&gt;</span><span class="w">
              </span><span class="ss">true</span><span class="p">;</span><span class="w">
            </span><span class="p">_</span><span class="w"> </span><span class="p">-&gt;</span><span class="w">
              </span><span class="ss">false</span><span class="w">
          </span><span class="k">end</span><span class="w">
      </span><span class="k">end</span><span class="w">
  </span><span class="k">end</span><span class="p">.</span></code></pre>
<p>
If the host is not already an IP address it’ll first resolve the IPv4 address, and if that fails it’ll resolve the IPv6 address.</p>
<p>
This logic is <a href="https://github.com/benoitc/hackney/issues/206">problematic</a>, but <code class="inline">:hackney</code> will skip the above logic <a href="https://github.com/benoitc/hackney/blob/27bbf8ec11033e28c7b8424759851d2d9bafa887/src/hackney_connection.erl#L108-L119">when the <code class="inline">:inet6</code> flag</a> is set in the connect options. Unfortunately, <code class="inline">Honeybadger</code> didn’t allow for passing in options to <code class="inline">:hackney</code>. I opened a <a href="https://gi thub.com/honeybadger-io/honeybadger-elixir/pull/468">pull request</a> that allows for passing connect options. It has now been merged to main, and we can globally configure the options with:</p>
<pre><code class="makeup elixir"><span class="n">config</span><span class="w"> </span><span class="ss">:honeybadger</span><span class="p">,</span><span class="w">
  </span><span class="c1"># ...</span><span class="w">
  </span><span class="ss">hackney_opts</span><span class="p">:</span><span class="w"> </span><span class="p" data-group-id="4725853205-1">[</span><span class="ss">connect_options</span><span class="p">:</span><span class="w"> </span><span class="p" data-group-id="4725853205-2">[</span><span class="ss">:inet6</span><span class="p" data-group-id="4725853205-2">]</span><span class="p" data-group-id="4725853205-1">]</span></code></pre>
<p>
My instances are now successfully connecting to honeybadger over IPv6!</p>
<h2>
Good HTTP client practice</h2>
<p>
The above highlights an issue I see from time to time with libraries.</p>
<p>
When building a library you want to make things work out of the box. Developers shouldn’t have to implement their own HTTP client just to use Honeybadger. The problem is when you don’t give developers any control over the dependencies as seen above.</p>
<p>
At a minimum, it must be possible to pass on options to the underlying HTTP client. However, why force the use of a certain HTTP client dependency? What if I need to pool connections together across my system? Why can’t I just use my <a href="https://hex.pm/packages/finch"><code class="inline">Finch</code></a> process for all outgoing requests?</p>
<p>
That’s why I recommend setting up HTTP client adapters. It is what I’m doing with <a href="https://github.com/pow-auth/assent"><code class="inline">Assent</code></a>, defining a behaviour and using it for the built-in adapter(s):</p>
<pre><code class="makeup elixir"><span class="na">@type</span><span class="w"> </span><span class="n">method</span><span class="w"> </span><span class="o">::</span><span class="w"> </span><span class="ss">:get</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="ss">:post</span><span class="w">
</span><span class="na">@type</span><span class="w"> </span><span class="n">body</span><span class="w"> </span><span class="o">::</span><span class="w"> </span><span class="n">binary</span><span class="p" data-group-id="5599639459-1">(</span><span class="p" data-group-id="5599639459-1">)</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="no">nil</span><span class="w">
</span><span class="na">@type</span><span class="w"> </span><span class="n">headers</span><span class="w"> </span><span class="o">::</span><span class="w"> </span><span class="p" data-group-id="5599639459-2">[</span><span class="p" data-group-id="5599639459-3">{</span><span class="n">binary</span><span class="p" data-group-id="5599639459-4">(</span><span class="p" data-group-id="5599639459-4">)</span><span class="p">,</span><span class="w"> </span><span class="n">binary</span><span class="p" data-group-id="5599639459-5">(</span><span class="p" data-group-id="5599639459-5">)</span><span class="p" data-group-id="5599639459-3">}</span><span class="p" data-group-id="5599639459-2">]</span><span class="w">
</span><span class="na">@callback</span><span class="w"> </span><span class="n">request</span><span class="p" data-group-id="5599639459-6">(</span><span class="n">method</span><span class="p" data-group-id="5599639459-7">(</span><span class="p" data-group-id="5599639459-7">)</span><span class="p">,</span><span class="w"> </span><span class="n">binary</span><span class="p" data-group-id="5599639459-8">(</span><span class="p" data-group-id="5599639459-8">)</span><span class="p">,</span><span class="w"> </span><span class="n">body</span><span class="p" data-group-id="5599639459-9">(</span><span class="p" data-group-id="5599639459-9">)</span><span class="p">,</span><span class="w"> </span><span class="n">headers</span><span class="p" data-group-id="5599639459-10">(</span><span class="p" data-group-id="5599639459-10">)</span><span class="p">,</span><span class="w"> </span><span class="nc">Keyword</span><span class="o">.</span><span class="n">t</span><span class="p" data-group-id="5599639459-11">(</span><span class="p" data-group-id="5599639459-11">)</span><span class="p" data-group-id="5599639459-6">)</span><span class="w"> </span><span class="o">::</span><span class="w"> </span><span class="p" data-group-id="5599639459-12">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="n">map</span><span class="p" data-group-id="5599639459-13">(</span><span class="p" data-group-id="5599639459-13">)</span><span class="p" data-group-id="5599639459-12">}</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="p" data-group-id="5599639459-14">{</span><span class="ss">:error</span><span class="p">,</span><span class="w"> </span><span class="n">any</span><span class="p" data-group-id="5599639459-15">(</span><span class="p" data-group-id="5599639459-15">)</span><span class="p" data-group-id="5599639459-14">}</span></code></pre>
<p>
I’m doing the same with <a href="https://github.com/danschultzer/test_server"><code class="inline">TestServer</code></a> HTTP servers to support <code class="inline">Bandit</code>, <code class="inline">:cowboy</code>, and <code class="inline">:httpd</code>:</p>
<pre><code class="makeup elixir"><span class="na">@type</span><span class="w"> </span><span class="n">scheme</span><span class="w"> </span><span class="o">::</span><span class="w"> </span><span class="ss">:http</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="ss">:https</span><span class="w">
</span><span class="na">@type</span><span class="w"> </span><span class="n">instance</span><span class="w"> </span><span class="o">::</span><span class="w"> </span><span class="n">pid</span><span class="p" data-group-id="0007935638-1">(</span><span class="p" data-group-id="0007935638-1">)</span><span class="w">
</span><span class="na">@type</span><span class="w"> </span><span class="n">port_number</span><span class="w"> </span><span class="o">::</span><span class="w"> </span><span class="nc">:inet</span><span class="o">.</span><span class="n">port_number</span><span class="p" data-group-id="0007935638-2">(</span><span class="p" data-group-id="0007935638-2">)</span><span class="w">
</span><span class="na">@type</span><span class="w"> </span><span class="n">tls_options</span><span class="w"> </span><span class="o">::</span><span class="w"> </span><span class="n">keyword</span><span class="p" data-group-id="0007935638-3">(</span><span class="p" data-group-id="0007935638-3">)</span><span class="w">
</span><span class="na">@type</span><span class="w"> </span><span class="n">server_options</span><span class="w"> </span><span class="o">::</span><span class="w"> </span><span class="n">keyword</span><span class="p" data-group-id="0007935638-4">(</span><span class="p" data-group-id="0007935638-4">)</span><span class="w">

</span><span class="na">@callback</span><span class="w"> </span><span class="n">start</span><span class="p" data-group-id="0007935638-5">(</span><span class="n">instance</span><span class="p" data-group-id="0007935638-6">(</span><span class="p" data-group-id="0007935638-6">)</span><span class="p">,</span><span class="w"> </span><span class="n">port_number</span><span class="p" data-group-id="0007935638-7">(</span><span class="p" data-group-id="0007935638-7">)</span><span class="p">,</span><span class="w"> </span><span class="n">scheme</span><span class="p" data-group-id="0007935638-8">(</span><span class="p" data-group-id="0007935638-8">)</span><span class="p">,</span><span class="w"> </span><span class="n">tls_options</span><span class="p" data-group-id="0007935638-9">(</span><span class="p" data-group-id="0007935638-9">)</span><span class="p">,</span><span class="w"> </span><span class="n">server_options</span><span class="p" data-group-id="0007935638-10">(</span><span class="p" data-group-id="0007935638-10">)</span><span class="p" data-group-id="0007935638-5">)</span><span class="w"> </span><span class="o">::</span><span class="w"> </span><span class="p" data-group-id="0007935638-11">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="n">pid</span><span class="p" data-group-id="0007935638-12">(</span><span class="p" data-group-id="0007935638-12">)</span><span class="p">,</span><span class="w"> </span><span class="n">server_options</span><span class="p" data-group-id="0007935638-13">(</span><span class="p" data-group-id="0007935638-13">)</span><span class="p" data-group-id="0007935638-11">}</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="p" data-group-id="0007935638-14">{</span><span class="ss">:error</span><span class="p">,</span><span class="w"> </span><span class="n">any</span><span class="p" data-group-id="0007935638-15">(</span><span class="p" data-group-id="0007935638-15">)</span><span class="p" data-group-id="0007935638-14">}</span><span class="w">
</span><span class="na">@callback</span><span class="w"> </span><span class="n">stop</span><span class="p" data-group-id="0007935638-16">(</span><span class="n">instance</span><span class="p" data-group-id="0007935638-17">(</span><span class="p" data-group-id="0007935638-17">)</span><span class="p">,</span><span class="w"> </span><span class="n">server_options</span><span class="p" data-group-id="0007935638-18">(</span><span class="p" data-group-id="0007935638-18">)</span><span class="p" data-group-id="0007935638-16">)</span><span class="w"> </span><span class="o">::</span><span class="w"> </span><span class="ss">:ok</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="p" data-group-id="0007935638-19">{</span><span class="ss">:error</span><span class="p">,</span><span class="w"> </span><span class="n">any</span><span class="p" data-group-id="0007935638-20">(</span><span class="p" data-group-id="0007935638-20">)</span><span class="p" data-group-id="0007935638-19">}</span></code></pre>
<p>
<a href="https://genserver.social/users/sorentwo">Parker Selbert</a> is currently <a href="https://github.com/honeybadger-io/honeybadger-elixir/issues/467#issuecomment-1419848736">working</a> on introducing this to the <code class="inline">Honeybadger</code> library! </p>
<p>
This helps developers manage their dependency graphs, and it allows developers in unique situations to solve the problems without having to fork, patch, and then wait for an upstream release with a fix.</p>

      ]]>
    </content>
  </entry>
  
  <entry>
    <title>Dynamic image generation with Elixir</title>
    <link href="https://danschultzer.com/posts/dynamic-image-generation-with-elixir" />
    <id>https://danschultzer.com/posts/dynamic-image-generation-with-elixir</id>
    <updated>2023-02-13T00:00:00Z</updated>
    <summary>How I use the Image library to dynamically generate Open Graph PNG images for my blog posts from SVG.</summary>
    <content type="html">
      <![CDATA[
        <p>
Yesterday I shared the blog post detailing <a href="/posts/welcome-to-my-blog">how I’ve built this blog</a>. I felt the link preview was lacking without any image.</p>
<table>
  <tr>    <td style="vertical-align:middle;">
    <img src="/images/posts/2023/02-13-dynamic-image-generation-with-elixir-mastodon.png" alt="Mastodon post without preview image" />
  </td>
  <td style="vertical-align:middle;">
    <img src="/images/posts/2023/02-13-dynamic-image-generation-with-elixir-slack.png" alt="Slack post without preview image" />
  </td>  </tr></table>
<p>
Sure, I could add header images to the blog post, but what if I could just dynamically generate them on the fly? Already optimized for Open Graph?</p>
<p>
Luckily that was super easy using <a href="https://github.com/elixir-image/image"><code class="inline">Image</code></a> library by the always diligent <a href="https://twitter.com/@kipcole9">Kip Cole</a>. What took me the longest was just figuring out what makes for a good preview image! I used the <a href="https://editor.method.ac/">Method Draw</a> SVG editor to design the SVG.</p>
<p>
First, we’ll add <code class="inline">Image</code> to the dependencies:</p>
<pre><code class="makeup elixir"><span class="c1"># mix.exs</span><span class="w">
</span><span class="kd">defmodule</span><span class="w"> </span><span class="nc">MyBlog.MixProject</span><span class="w"> </span><span class="k" data-group-id="2042204934-1">do</span><span class="w">
  </span><span class="kn">use</span><span class="w"> </span><span class="nc">Mix.Project</span><span class="w">

  </span><span class="c1"># ...</span><span class="w">

  </span><span class="kd">def</span><span class="w"> </span><span class="nf">deps</span><span class="w"> </span><span class="k" data-group-id="2042204934-2">do</span><span class="w">
    </span><span class="p" data-group-id="2042204934-3">[</span><span class="w">
      </span><span class="c1"># ...</span><span class="w">
      </span><span class="p" data-group-id="2042204934-4">{</span><span class="ss">:image</span><span class="p">,</span><span class="w"> </span><span class="s">&quot;~&gt; 0.33&quot;</span><span class="p" data-group-id="2042204934-4">}</span><span class="w">
    </span><span class="p" data-group-id="2042204934-3">]</span><span class="w">
  </span><span class="k" data-group-id="2042204934-2">end</span><span class="w">

  </span><span class="c1">#...</span><span class="w">
</span><span class="k" data-group-id="2042204934-1">end</span></code></pre>
<p>
Now we’ll update our <code class="inline">Blog.Post</code> module to generate an Open Graph image that’s 1200 by 600 pixels.</p>
<pre><code class="makeup elixir"><span class="c1"># lib/my_blog/blog/post.ex</span><span class="w">
</span><span class="kd">defmodule</span><span class="w"> </span><span class="nc">MyBlog.Blog.Post</span><span class="w"> </span><span class="k" data-group-id="8254273076-1">do</span><span class="w">
  </span><span class="na">@enforce_keys</span><span class="w"> </span><span class="p" data-group-id="8254273076-2">[</span><span class="ss">:id</span><span class="p">,</span><span class="w"> </span><span class="ss">:og_image</span><span class="p">,</span><span class="w"> </span><span class="ss">:title</span><span class="p">,</span><span class="w"> </span><span class="ss">:body</span><span class="p">,</span><span class="w"> </span><span class="ss">:description</span><span class="p">,</span><span class="w"> </span><span class="ss">:tags</span><span class="p">,</span><span class="w"> </span><span class="ss">:date</span><span class="p" data-group-id="8254273076-2">]</span><span class="w">
  </span><span class="kd">defstruct</span><span class="w"> </span><span class="p" data-group-id="8254273076-3">[</span><span class="ss">:id</span><span class="p">,</span><span class="w"> </span><span class="ss">:og_image</span><span class="p">,</span><span class="w"> </span><span class="ss">:title</span><span class="p">,</span><span class="w"> </span><span class="ss">:body</span><span class="p">,</span><span class="w"> </span><span class="ss">:description</span><span class="p">,</span><span class="w"> </span><span class="ss">:tags</span><span class="p">,</span><span class="w"> </span><span class="ss">:date</span><span class="p" data-group-id="8254273076-3">]</span><span class="w">

  </span><span class="kd">def</span><span class="w"> </span><span class="nf">build</span><span class="p" data-group-id="8254273076-4">(</span><span class="n">filename</span><span class="p">,</span><span class="w"> </span><span class="n">attrs</span><span class="p">,</span><span class="w"> </span><span class="n">body</span><span class="p" data-group-id="8254273076-4">)</span><span class="w"> </span><span class="k" data-group-id="8254273076-5">do</span><span class="w">
    </span><span class="p" data-group-id="8254273076-6">[</span><span class="n">year</span><span class="p">,</span><span class="w"> </span><span class="n">month_day_id</span><span class="p" data-group-id="8254273076-6">]</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">filename</span><span class="w"> </span><span class="o">|&gt;</span><span class="w"> </span><span class="nc">Path</span><span class="o">.</span><span class="n">rootname</span><span class="p" data-group-id="8254273076-7">(</span><span class="p" data-group-id="8254273076-7">)</span><span class="w"> </span><span class="o">|&gt;</span><span class="w"> </span><span class="nc">Path</span><span class="o">.</span><span class="n">split</span><span class="p" data-group-id="8254273076-8">(</span><span class="p" data-group-id="8254273076-8">)</span><span class="w"> </span><span class="o">|&gt;</span><span class="w"> </span><span class="nc">Enum</span><span class="o">.</span><span class="n">take</span><span class="p" data-group-id="8254273076-9">(</span><span class="o">-</span><span class="mi">2</span><span class="p" data-group-id="8254273076-9">)</span><span class="w">
    </span><span class="p" data-group-id="8254273076-10">[</span><span class="n">month</span><span class="p">,</span><span class="w"> </span><span class="n">day</span><span class="p">,</span><span class="w"> </span><span class="n">id</span><span class="p" data-group-id="8254273076-10">]</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">String</span><span class="o">.</span><span class="n">split</span><span class="p" data-group-id="8254273076-11">(</span><span class="n">month_day_id</span><span class="p">,</span><span class="w"> </span><span class="s">&quot;-&quot;</span><span class="p">,</span><span class="w"> </span><span class="ss">parts</span><span class="p">:</span><span class="w"> </span><span class="mi">3</span><span class="p" data-group-id="8254273076-11">)</span><span class="w">
    </span><span class="n">date</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">Date</span><span class="o">.</span><span class="n">from_iso8601!</span><span class="p" data-group-id="8254273076-12">(</span><span class="s">&quot;</span><span class="si" data-group-id="8254273076-13">#{</span><span class="n">year</span><span class="si" data-group-id="8254273076-13">}</span><span class="s">-</span><span class="si" data-group-id="8254273076-14">#{</span><span class="n">month</span><span class="si" data-group-id="8254273076-14">}</span><span class="s">-</span><span class="si" data-group-id="8254273076-15">#{</span><span class="n">day</span><span class="si" data-group-id="8254273076-15">}</span><span class="s">&quot;</span><span class="p" data-group-id="8254273076-12">)</span><span class="w">
    </span><span class="n">attrs</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">Map</span><span class="o">.</span><span class="n">put</span><span class="p" data-group-id="8254273076-16">(</span><span class="n">attrs</span><span class="p">,</span><span class="w"> </span><span class="ss">:og_image</span><span class="p">,</span><span class="w"> </span><span class="n">generate_og_image</span><span class="p" data-group-id="8254273076-17">(</span><span class="n">year</span><span class="p">,</span><span class="w"> </span><span class="n">filename</span><span class="p">,</span><span class="w"> </span><span class="n">attrs</span><span class="o">.</span><span class="n">title</span><span class="p">,</span><span class="w"> </span><span class="n">attrs</span><span class="o">.</span><span class="n">tags</span><span class="p" data-group-id="8254273076-17">)</span><span class="p" data-group-id="8254273076-16">)</span><span class="w">
    </span><span class="n">struct!</span><span class="p" data-group-id="8254273076-18">(</span><span class="bp">__MODULE__</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="8254273076-19">[</span><span class="ss">id</span><span class="p">:</span><span class="w"> </span><span class="n">id</span><span class="p">,</span><span class="w"> </span><span class="ss">date</span><span class="p">:</span><span class="w"> </span><span class="n">date</span><span class="p">,</span><span class="w"> </span><span class="ss">body</span><span class="p">:</span><span class="w"> </span><span class="n">body</span><span class="p" data-group-id="8254273076-19">]</span><span class="w"> </span><span class="o">++</span><span class="w"> </span><span class="nc">Map</span><span class="o">.</span><span class="n">to_list</span><span class="p" data-group-id="8254273076-20">(</span><span class="n">attrs</span><span class="p" data-group-id="8254273076-20">)</span><span class="p" data-group-id="8254273076-18">)</span><span class="w">
  </span><span class="k" data-group-id="8254273076-5">end</span><span class="w">

  </span><span class="kd">defp</span><span class="w"> </span><span class="nf">generate_og_image</span><span class="p" data-group-id="8254273076-21">(</span><span class="n">year</span><span class="p">,</span><span class="w"> </span><span class="n">filename</span><span class="p">,</span><span class="w"> </span><span class="n">title</span><span class="p">,</span><span class="w"> </span><span class="n">tags</span><span class="p" data-group-id="8254273076-21">)</span><span class="w"> </span><span class="k" data-group-id="8254273076-22">do</span><span class="w">
    </span><span class="p" data-group-id="8254273076-23">{</span><span class="n">filename</span><span class="p">,</span><span class="w"> </span><span class="n">basename</span><span class="p" data-group-id="8254273076-23">}</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">og_image_paths</span><span class="p" data-group-id="8254273076-24">(</span><span class="n">year</span><span class="p">,</span><span class="w"> </span><span class="n">filename</span><span class="p" data-group-id="8254273076-24">)</span><span class="w">
    </span><span class="p" data-group-id="8254273076-25">{</span><span class="n">title_line_1</span><span class="p">,</span><span class="w"> </span><span class="n">title_line_2</span><span class="p" data-group-id="8254273076-25">}</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">og_image_title_lines</span><span class="p" data-group-id="8254273076-26">(</span><span class="n">title</span><span class="p" data-group-id="8254273076-26">)</span><span class="w">
    </span><span class="n">tags</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">Enum</span><span class="o">.</span><span class="n">join</span><span class="p" data-group-id="8254273076-27">(</span><span class="n">tags</span><span class="p">,</span><span class="w"> </span><span class="s">&quot;, &quot;</span><span class="p" data-group-id="8254273076-27">)</span><span class="w">

    </span><span class="n">svg</span><span class="w"> </span><span class="o">=</span><span class="w">
      </span><span class="s">&quot;&quot;&quot;
      &lt;svg viewbox=&quot;0 0 1200 600&quot; width=&quot;1200&quot; height=&quot;600&quot; xmlns=&quot;http://www.w3.org/2000/svg&quot;&gt;
        &lt;defs&gt;
          &lt;linearGradient y2=&quot;1&quot; x2=&quot;1&quot; y1=&quot;0.14844&quot; x1=&quot;0.53125&quot; id=&quot;gradient&quot;&gt;
          &lt;stop offset=&quot;0&quot; stop-opacity=&quot;0.99609&quot; stop-color=&quot;#5b21b6&quot;/&gt;
          &lt;stop offset=&quot;0.99219&quot; stop-opacity=&quot;0.97656&quot; stop-color=&quot;#ff8300&quot;/&gt;
          &lt;/linearGradient&gt;
        &lt;/defs&gt;
        &lt;g&gt;
          &lt;rect stroke=&quot;#000&quot; height=&quot;800&quot; width=&quot;1800&quot; y=&quot;0&quot; x=&quot;0&quot; stroke-width=&quot;0&quot; fill=&quot;url(#gradient)&quot;/&gt;
          &lt;text font-style=&quot;normal&quot; font-weight=&quot;normal&quot; xml:space=&quot;preserve&quot; text-anchor=&quot;start&quot; font-family=&quot;&#39;Alumni Sans&#39;&quot; font-size=&quot;70&quot; y=&quot;250&quot; x=&quot;100&quot; stroke-width=&quot;0&quot; stroke=&quot;#000&quot; fill=&quot;#f8fafc&quot;&gt;</span><span class="si" data-group-id="8254273076-28">#{</span><span class="n">title_line_1</span><span class="si" data-group-id="8254273076-28">}</span><span class="s">&lt;/text&gt;
          &lt;text font-style=&quot;normal&quot; font-weight=&quot;normal&quot; xml:space=&quot;preserve&quot; text-anchor=&quot;start&quot; font-family=&quot;&#39;Alumni Sans&#39;&quot; font-size=&quot;70&quot; y=&quot;350&quot; x=&quot;100&quot; stroke-width=&quot;0&quot; stroke=&quot;#000&quot; fill=&quot;#f8fafc&quot;&gt;</span><span class="si" data-group-id="8254273076-29">#{</span><span class="n">title_line_2</span><span class="si" data-group-id="8254273076-29">}</span><span class="s">&lt;/text&gt;
          &lt;text font-style=&quot;normal&quot; font-weight=&quot;normal&quot; xml:space=&quot;preserve&quot; text-anchor=&quot;start&quot; font-family=&quot;&#39;Alumni Sans&#39;&quot; font-size=&quot;30&quot; y=&quot;550&quot; x=&quot;50&quot; stroke-width=&quot;0&quot; stroke=&quot;#000&quot; fill=&quot;#f8fafc&quot; opacity=&quot;0.5&quot;&gt;By Dan Schultzer&lt;/text&gt;
          &lt;text font-style=&quot;normal&quot; font-weight=&quot;normal&quot; xml:space=&quot;preserve&quot; text-anchor=&quot;end&quot; font-family=&quot;&#39;Alumni Sans&#39;&quot; font-size=&quot;30&quot; y=&quot;550&quot; x=&quot;1150&quot; stroke-width=&quot;0&quot; stroke=&quot;#000&quot; fill=&quot;#f8fafc&quot; opacity=&quot;0.5&quot;&gt;</span><span class="si" data-group-id="8254273076-30">#{</span><span class="n">tags</span><span class="si" data-group-id="8254273076-30">}</span><span class="s">&lt;/text&gt;
        &lt;/g&gt;
      &lt;/svg&gt;
      &quot;&quot;&quot;</span><span class="w">

    </span><span class="n">write_og_image</span><span class="p" data-group-id="8254273076-31">(</span><span class="n">filename</span><span class="p">,</span><span class="w"> </span><span class="n">svg</span><span class="p" data-group-id="8254273076-31">)</span><span class="w">

    </span><span class="p" data-group-id="8254273076-32">%{</span><span class="ss">year</span><span class="p">:</span><span class="w"> </span><span class="n">year</span><span class="p">,</span><span class="w"> </span><span class="ss">basename</span><span class="p">:</span><span class="w"> </span><span class="n">basename</span><span class="p" data-group-id="8254273076-32">}</span><span class="w">
  </span><span class="k" data-group-id="8254273076-22">end</span><span class="w">

  </span><span class="kd">defp</span><span class="w"> </span><span class="nf">og_image_paths</span><span class="p" data-group-id="8254273076-33">(</span><span class="n">year</span><span class="p">,</span><span class="w"> </span><span class="n">filename</span><span class="p" data-group-id="8254273076-33">)</span><span class="w"> </span><span class="k" data-group-id="8254273076-34">do</span><span class="w">
    </span><span class="p" data-group-id="8254273076-35">[</span><span class="n">root_dir</span><span class="p">,</span><span class="w"> </span><span class="n">file</span><span class="p" data-group-id="8254273076-35">]</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">filename</span><span class="w"> </span><span class="o">|&gt;</span><span class="w"> </span><span class="nc">Path</span><span class="o">.</span><span class="n">rootname</span><span class="p" data-group-id="8254273076-36">(</span><span class="p" data-group-id="8254273076-36">)</span><span class="w"> </span><span class="o">|&gt;</span><span class="w"> </span><span class="nc">String</span><span class="o">.</span><span class="n">split</span><span class="p" data-group-id="8254273076-37">(</span><span class="nc">Path</span><span class="o">.</span><span class="n">join</span><span class="p" data-group-id="8254273076-38">(</span><span class="s">&quot;posts&quot;</span><span class="p">,</span><span class="w"> </span><span class="n">year</span><span class="p" data-group-id="8254273076-38">)</span><span class="p" data-group-id="8254273076-37">)</span><span class="w">
    </span><span class="n">basename</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">Path</span><span class="o">.</span><span class="n">basename</span><span class="p" data-group-id="8254273076-39">(</span><span class="n">file</span><span class="p">,</span><span class="w"> </span><span class="s">&quot;.md&quot;</span><span class="p" data-group-id="8254273076-39">)</span><span class="w"> </span><span class="o">&lt;&gt;</span><span class="w"> </span><span class="s">&quot;.open-graph.png&quot;</span><span class="w">
    </span><span class="n">filename</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">Path</span><span class="o">.</span><span class="n">join</span><span class="p" data-group-id="8254273076-40">(</span><span class="p" data-group-id="8254273076-41">[</span><span class="n">root_dir</span><span class="p">,</span><span class="w"> </span><span class="s">&quot;static&quot;</span><span class="p">,</span><span class="w"> </span><span class="s">&quot;images&quot;</span><span class="p">,</span><span class="w"> </span><span class="s">&quot;posts&quot;</span><span class="p">,</span><span class="w"> </span><span class="n">year</span><span class="p">,</span><span class="w"> </span><span class="n">basename</span><span class="p" data-group-id="8254273076-41">]</span><span class="p" data-group-id="8254273076-40">)</span><span class="w">

    </span><span class="nc">File</span><span class="o">.</span><span class="n">mkdir_p!</span><span class="p" data-group-id="8254273076-42">(</span><span class="nc">Path</span><span class="o">.</span><span class="n">dirname</span><span class="p" data-group-id="8254273076-43">(</span><span class="n">filename</span><span class="p" data-group-id="8254273076-43">)</span><span class="p" data-group-id="8254273076-42">)</span><span class="w">

    </span><span class="p" data-group-id="8254273076-44">{</span><span class="n">filename</span><span class="p">,</span><span class="w"> </span><span class="n">basename</span><span class="p" data-group-id="8254273076-44">}</span><span class="w">
  </span><span class="k" data-group-id="8254273076-34">end</span><span class="w">

  </span><span class="na">@max_length</span><span class="w"> </span><span class="mi">31</span><span class="w">

  </span><span class="kd">defp</span><span class="w"> </span><span class="nf">og_image_title_lines</span><span class="p" data-group-id="8254273076-45">(</span><span class="n">title</span><span class="p" data-group-id="8254273076-45">)</span><span class="w"> </span><span class="k" data-group-id="8254273076-46">do</span><span class="w">
    </span><span class="n">title</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="nc">String</span><span class="o">.</span><span class="n">split</span><span class="p" data-group-id="8254273076-47">(</span><span class="s">&quot; &quot;</span><span class="p" data-group-id="8254273076-47">)</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="nc">Enum</span><span class="o">.</span><span class="n">reduce_while</span><span class="p" data-group-id="8254273076-48">(</span><span class="p" data-group-id="8254273076-49">{</span><span class="s">&quot;&quot;</span><span class="p">,</span><span class="w"> </span><span class="s">&quot;&quot;</span><span class="p" data-group-id="8254273076-49">}</span><span class="p">,</span><span class="w"> </span><span class="k" data-group-id="8254273076-50">fn</span><span class="w"> </span><span class="n">word</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="8254273076-51">{</span><span class="n">title_line_1</span><span class="p">,</span><span class="w"> </span><span class="n">title_line_2</span><span class="p" data-group-id="8254273076-51">}</span><span class="w"> </span><span class="o">-&gt;</span><span class="w">
      </span><span class="k">cond</span><span class="w"> </span><span class="k" data-group-id="8254273076-52">do</span><span class="w">
        </span><span class="nc">String</span><span class="o">.</span><span class="n">length</span><span class="p" data-group-id="8254273076-53">(</span><span class="n">title_line_1</span><span class="w"> </span><span class="o">&lt;&gt;</span><span class="w"> </span><span class="s">&quot; &quot;</span><span class="w"> </span><span class="o">&lt;&gt;</span><span class="w"> </span><span class="n">word</span><span class="p" data-group-id="8254273076-53">)</span><span class="w"> </span><span class="o">&lt;=</span><span class="w"> </span><span class="na">@max_length</span><span class="w"> </span><span class="o">-&gt;</span><span class="w"> </span><span class="p" data-group-id="8254273076-54">{</span><span class="ss">:cont</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="8254273076-55">{</span><span class="n">title_line_1</span><span class="w"> </span><span class="o">&lt;&gt;</span><span class="w"> </span><span class="s">&quot; &quot;</span><span class="w"> </span><span class="o">&lt;&gt;</span><span class="w"> </span><span class="n">word</span><span class="p">,</span><span class="w"> </span><span class="n">title_line_2</span><span class="p" data-group-id="8254273076-55">}</span><span class="p" data-group-id="8254273076-54">}</span><span class="w">
        </span><span class="nc">String</span><span class="o">.</span><span class="n">length</span><span class="p" data-group-id="8254273076-56">(</span><span class="n">title_line_2</span><span class="w"> </span><span class="o">&lt;&gt;</span><span class="w"> </span><span class="s">&quot; &quot;</span><span class="w"> </span><span class="o">&lt;&gt;</span><span class="w"> </span><span class="n">word</span><span class="p" data-group-id="8254273076-56">)</span><span class="w"> </span><span class="o">&lt;=</span><span class="w"> </span><span class="p" data-group-id="8254273076-57">(</span><span class="na">@max_length</span><span class="w"> </span><span class="o">-</span><span class="w"> </span><span class="mi">3</span><span class="p" data-group-id="8254273076-57">)</span><span class="w"> </span><span class="o">-&gt;</span><span class="w"> </span><span class="p" data-group-id="8254273076-58">{</span><span class="ss">:cont</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="8254273076-59">{</span><span class="n">title_line_1</span><span class="p">,</span><span class="w"> </span><span class="n">title_line_2</span><span class="w"> </span><span class="o">&lt;&gt;</span><span class="w"> </span><span class="s">&quot; &quot;</span><span class="w"> </span><span class="o">&lt;&gt;</span><span class="w"> </span><span class="n">word</span><span class="p" data-group-id="8254273076-59">}</span><span class="p" data-group-id="8254273076-58">}</span><span class="w">
        </span><span class="no">true</span><span class="w"> </span><span class="o">-&gt;</span><span class="w"> </span><span class="p" data-group-id="8254273076-60">{</span><span class="ss">:halt</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="8254273076-61">{</span><span class="n">title_line_1</span><span class="p">,</span><span class="w"> </span><span class="n">title_line_2</span><span class="w"> </span><span class="o">&lt;&gt;</span><span class="w"> </span><span class="s">&quot;...&quot;</span><span class="p" data-group-id="8254273076-61">}</span><span class="p" data-group-id="8254273076-60">}</span><span class="w">
      </span><span class="k" data-group-id="8254273076-52">end</span><span class="w">
    </span><span class="k" data-group-id="8254273076-50">end</span><span class="p" data-group-id="8254273076-48">)</span><span class="w">
  </span><span class="k" data-group-id="8254273076-46">end</span><span class="w">

  </span><span class="kd">defp</span><span class="w"> </span><span class="nf">write_og_image</span><span class="p" data-group-id="8254273076-62">(</span><span class="n">filename</span><span class="p">,</span><span class="w"> </span><span class="n">svg</span><span class="p" data-group-id="8254273076-62">)</span><span class="w"> </span><span class="k" data-group-id="8254273076-63">do</span><span class="w">
    </span><span class="p" data-group-id="8254273076-64">{</span><span class="n">image</span><span class="p">,</span><span class="w"> </span><span class="bp">_</span><span class="p" data-group-id="8254273076-64">}</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">Vix.Vips.Operation</span><span class="o">.</span><span class="n">svgload_buffer!</span><span class="p" data-group-id="8254273076-65">(</span><span class="n">svg</span><span class="p" data-group-id="8254273076-65">)</span><span class="w">

    </span><span class="nc">Image</span><span class="o">.</span><span class="n">write!</span><span class="p" data-group-id="8254273076-66">(</span><span class="n">image</span><span class="p">,</span><span class="w"> </span><span class="n">filename</span><span class="p" data-group-id="8254273076-66">)</span><span class="w">
  </span><span class="k" data-group-id="8254273076-63">end</span><span class="w">
</span><span class="k" data-group-id="8254273076-1">end</span></code></pre>
<p>
There are a few things happening here:</p>
<ul>
  <li>
Derive a filename based on the filename of the blog post with <code class="inline">.open-graph.png</code> as the extension  </li>
  <li>
Create <code class="inline">images/posts/:year/</code> folder path inside <code class="inline">priv/static/</code>  </li>
  <li>
Split the title into two lines as SVG doesn’t support multiline text  </li>
  <li>
Truncate the second title line if it’s too long  </li>
  <li>
Generate an image using SVG and save it  </li>
</ul>
<p>
Now all you need is to add the Open Graph metatags to <code class="inline">&lt;head&gt;</code> for the pages with a post:</p>
<pre><code class="makeup elixir"><span class="c1"># lib/my_blog_web/components/layouts/root.html.heex</span><span class="w">
</span><span class="c1"># ...</span><span class="w">
</span><span class="o">&lt;</span><span class="n">meta</span><span class="w"> </span><span class="ss">:if</span><span class="o">=</span><span class="p" data-group-id="0462645125-1">{</span><span class="n">assign</span><span class="p" data-group-id="0462645125-2">[</span><span class="ss">:post</span><span class="p" data-group-id="0462645125-2">]</span><span class="p" data-group-id="0462645125-1">}</span><span class="w"> </span><span class="n">property</span><span class="o">=</span><span class="s">&quot;og:type&quot;</span><span class="w"> </span><span class="n">content</span><span class="o">=</span><span class="s">&quot;article&quot;</span><span class="w"> </span><span class="o">/</span><span class="o">&gt;</span><span class="w">
</span><span class="o">&lt;</span><span class="n">meta</span><span class="w"> </span><span class="ss">:if</span><span class="o">=</span><span class="p" data-group-id="0462645125-3">{</span><span class="n">assign</span><span class="p" data-group-id="0462645125-4">[</span><span class="ss">:post</span><span class="p" data-group-id="0462645125-4">]</span><span class="p" data-group-id="0462645125-3">}</span><span class="w"> </span><span class="n">property</span><span class="o">=</span><span class="s">&quot;og:image&quot;</span><span class="w"> </span><span class="n">content</span><span class="o">=</span><span class="p" data-group-id="0462645125-5">{</span><span class="n">url</span><span class="p" data-group-id="0462645125-6">(</span><span class="sx">~p&quot;/images/posts/</span><span class="si" data-group-id="0462645125-7">#{</span><span class="na">@post</span><span class="o">.</span><span class="n">og_image</span><span class="o">.</span><span class="n">year</span><span class="si" data-group-id="0462645125-7">}</span><span class="sx">/</span><span class="si" data-group-id="0462645125-8">#{</span><span class="na">@post</span><span class="o">.</span><span class="n">og_image</span><span class="o">.</span><span class="n">basename</span><span class="si" data-group-id="0462645125-8">}</span><span class="sx">&quot;</span><span class="p" data-group-id="0462645125-6">)</span><span class="p" data-group-id="0462645125-5">}</span><span class="w"> </span><span class="o">/</span><span class="o">&gt;</span><span class="w">
</span><span class="c1"># ...</span><span class="w">
</span><span class="o">&lt;</span><span class="n">meta</span><span class="w"> </span><span class="ss">:if</span><span class="o">=</span><span class="p" data-group-id="0462645125-9">{</span><span class="n">assign</span><span class="p" data-group-id="0462645125-10">[</span><span class="ss">:post</span><span class="p" data-group-id="0462645125-10">]</span><span class="p" data-group-id="0462645125-9">}</span><span class="w"> </span><span class="n">property</span><span class="o">=</span><span class="s">&quot;twitter:card&quot;</span><span class="w"> </span><span class="n">content</span><span class="o">=</span><span class="s">&quot;summary_large_image&quot;</span><span class="w"> </span><span class="o">/</span><span class="o">&gt;</span><span class="w">
</span><span class="o">&lt;</span><span class="n">meta</span><span class="w"> </span><span class="ss">:if</span><span class="o">=</span><span class="p" data-group-id="0462645125-11">{</span><span class="n">assign</span><span class="p" data-group-id="0462645125-12">[</span><span class="ss">:post</span><span class="p" data-group-id="0462645125-12">]</span><span class="p" data-group-id="0462645125-11">}</span><span class="w"> </span><span class="n">property</span><span class="o">=</span><span class="s">&quot;twitter:image&quot;</span><span class="w"> </span><span class="n">content</span><span class="o">=</span><span class="p" data-group-id="0462645125-13">{</span><span class="n">url</span><span class="p" data-group-id="0462645125-14">(</span><span class="sx">~p&quot;/images/posts/</span><span class="si" data-group-id="0462645125-15">#{</span><span class="na">@post</span><span class="o">.</span><span class="n">og_image</span><span class="o">.</span><span class="n">year</span><span class="si" data-group-id="0462645125-15">}</span><span class="sx">/</span><span class="si" data-group-id="0462645125-16">#{</span><span class="na">@post</span><span class="o">.</span><span class="n">og_image</span><span class="o">.</span><span class="n">basename</span><span class="si" data-group-id="0462645125-16">}</span><span class="sx">&quot;</span><span class="p" data-group-id="0462645125-14">)</span><span class="p" data-group-id="0462645125-13">}</span><span class="w"> </span><span class="o">/</span><span class="o">&gt;</span></code></pre>
<p>
Since the images are autogenerated we should ignore them in <code class="inline">.gitignore</code> so they don’t show up in the working tree:</p>
<pre><code class="bash"># .gitignore

# Ignore assets that are produced by build tools.
/priv/static/assets/
/priv/static/images/posts/*/*.open-graph.png</code></pre>
<p>
You are done! Spin up the server with <code class="inline">mix phx.server</code> and you should see <code class="inline">images/posts/:year/*.open-graph.png</code> file(s) in <code class="inline">priv/static/</code> there should be a generated Open Graph image looking something like this:</p>
<table>
  <tr>    <td style="vertical-align:middle;">
    <img src="/images/posts/2023/02-13-dynamic-image-generation-with-elixir-generated.png" alt="Dynamically generated Open Graph image" />
  </td>
  <td style="vertical-align:middle;">
    <img src="/images/posts/2023/02-13-dynamic-image-generation-with-elixir-url-preview.png" alt="Link preview showing now with the Open Graph image" />
  </td>  </tr></table>
<p>
Wow, huh, guess this solidifies that Phoenix with NimblePublisher is an excellent choice for blogging.</p>
<h2>
Custom fonts</h2>
<p>
I’m using a font that doesn’t exist in the debian image I deploy and it ends up using a default system font. How do we fix that? Easy!</p>
<p>
First, put your font in <code class="inline">./assets/fonts</code>. Now in your <code class="inline">Dockerfile</code>, make sure the font is installed before <code class="inline">RUN mix deps.compile</code> gets called:</p>
<pre><code class="dockerfile"># Install font used for generated images
COPY ./assets/fonts/AlumniSans-VariableFont_wght.ttf ./
RUN mkdir -p /usr/share/fonts/truetype/
RUN install -m644 AlumniSans-VariableFont_wght.ttf /usr/share/fonts/truetype/
RUN rm ./AlumniSans-VariableFont_wght.ttf</code></pre>
<h2>
Performance</h2>
<p>
I wondered how much of a performance penalty I added to my build process by doing this. After all, I do generate an image for every single blog post. If it takes any significant amount of time then I would have to look into caching now.</p>
<p>
Let’s figure out what the median duration run looks like! I’m adding this code to my <code class="inline">MyApp.Blog.Post.build/3</code> function:</p>
<pre><code class="makeup elixir"><span class="mi">1</span><span class="o">..</span><span class="mi">100</span><span class="w"> </span><span class="c1">#=&gt; 1..100</span><span class="w">
</span><span class="o">|&gt;</span><span class="w"> </span><span class="nc">Enum</span><span class="o">.</span><span class="n">map</span><span class="p" data-group-id="6447722411-1">(</span><span class="k" data-group-id="6447722411-2">fn</span><span class="w"> </span><span class="bp">_</span><span class="w"> </span><span class="o">-&gt;</span><span class="w">
  </span><span class="n">start_time</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">System</span><span class="o">.</span><span class="n">monotonic_time</span><span class="p" data-group-id="6447722411-3">(</span><span class="p" data-group-id="6447722411-3">)</span><span class="w">
  </span><span class="n">generate_og_image</span><span class="p" data-group-id="6447722411-4">(</span><span class="n">year</span><span class="p">,</span><span class="w"> </span><span class="n">filename</span><span class="p">,</span><span class="w"> </span><span class="n">attrs</span><span class="o">.</span><span class="n">title</span><span class="p">,</span><span class="w"> </span><span class="n">attrs</span><span class="o">.</span><span class="n">tags</span><span class="p" data-group-id="6447722411-4">)</span><span class="w">
  </span><span class="nc">System</span><span class="o">.</span><span class="n">monotonic_time</span><span class="p" data-group-id="6447722411-5">(</span><span class="p" data-group-id="6447722411-5">)</span><span class="w"> </span><span class="o">-</span><span class="w"> </span><span class="n">start_time</span><span class="w">
</span><span class="k" data-group-id="6447722411-2">end</span><span class="p" data-group-id="6447722411-1">)</span><span class="w"> </span><span class="c1">#=&gt; [...]</span><span class="w">
</span><span class="o">|&gt;</span><span class="w"> </span><span class="nc">Enum</span><span class="o">.</span><span class="n">sort</span><span class="p" data-group-id="6447722411-6">(</span><span class="p" data-group-id="6447722411-6">)</span><span class="w"> </span><span class="c1">#=&gt; [...]</span><span class="w">
</span><span class="o">|&gt;</span><span class="w"> </span><span class="nc">Enum</span><span class="o">.</span><span class="n">at</span><span class="p" data-group-id="6447722411-7">(</span><span class="mi">49</span><span class="p" data-group-id="6447722411-7">)</span><span class="w"> </span><span class="c1">#=&gt; 17326042</span><span class="w">
</span><span class="o">|&gt;</span><span class="w"> </span><span class="nc">System</span><span class="o">.</span><span class="n">convert_time_unit</span><span class="p" data-group-id="6447722411-8">(</span><span class="ss">:native</span><span class="p">,</span><span class="w"> </span><span class="ss">:millisecond</span><span class="p" data-group-id="6447722411-8">)</span><span class="w"> </span><span class="c1">#=&gt; 17</span><span class="w">
</span><span class="o">|&gt;</span><span class="w"> </span><span class="n">dbg</span><span class="p" data-group-id="6447722411-9">(</span><span class="p" data-group-id="6447722411-9">)</span></code></pre>
<p>
With a median duration of <code class="inline">~17ms</code> (and an average of <code class="inline">~20ms</code>) I don’t have to think about caching until I’m some hundred posts deep (and even then we’re only talking about seconds added to the build)!</p>

      ]]>
    </content>
  </entry>
  
  <entry>
    <title>Welcome to my blog</title>
    <link href="https://danschultzer.com/posts/welcome-to-my-blog" />
    <id>https://danschultzer.com/posts/welcome-to-my-blog</id>
    <updated>2023-02-12T00:00:00Z</updated>
    <summary>New year, new blog. Built with Phoenix, NimblePublisher, and Tailwind.</summary>
    <content type="html">
      <![CDATA[
        <p>
Why go through the effort of building my own blog and not just use <a href="https://ghost.org">ghost</a>?</p>
<p>
If you had asked me a few weeks ago I would have agreed that it sounds like a waste of time. I’ve tried so many variations of blogs. From early days with <a href="https://wordpress.com">WordPress</a>, to static website generators like <a href="https://jekyllrb.com">jekyll</a> and <a href="https://hexo.io">hexo</a>, to hosted versions like <a href="https://medium.com">medium</a>. All have their strengths and weaknesses, but I have never found one that suited me perfectly.</p>
<p>
Then I read José Valim’s <a href="https://dashbit.co/blog/welcome-to-our-blog-how-it-was-made">Dashbit blog post</a> on how they rolled their own blog. It appeared there was an effortless way to set up a blog in a Phoenix app with <a href="https://hex.pm/packages/nimble_publisher"><code class="inline">NimblePublisher</code></a>!</p>
<p>
I would have the best of both worlds. It’s kinda static with no database, it’s kinda a publishing system with Phoenix. And it’s all Elixir which is what I do <em>all day</em>. If one day I no longer work with Elixir professionally I would still have a good excuse to keep up with Phoenix development 😄</p>
<p>
So that’s what I did over the weekend. Below I’ll detail how I set it up in Phoenix 1.7 using LiveView.</p>
<p>
I also recommend reading <a href="https://elixirschool.com/en/lessons/misc/nimble_publisher">Elixir School’s lesson</a> and <a href="https://bernheisel.com/blog/moving-blog">David Bernheisel’s blog post</a>.</p>
<hr class="thin">
<p>
First, we’ll generate a new Phoenix app:</p>
<pre><code class="bash">mix phx.new my_blog --no-ecto</code></pre>
<p>
Add <code class="inline">NimblePublisher</code> to the dependencies:</p>
<pre><code class="makeup elixir"><span class="c1"># mix.exs</span><span class="w">
</span><span class="kd">defmodule</span><span class="w"> </span><span class="nc">MyBlog.MixProject</span><span class="w"> </span><span class="k" data-group-id="4432836159-1">do</span><span class="w">
  </span><span class="kn">use</span><span class="w"> </span><span class="nc">Mix.Project</span><span class="w">

  </span><span class="c1"># ...</span><span class="w">

  </span><span class="kd">def</span><span class="w"> </span><span class="nf">deps</span><span class="w"> </span><span class="k" data-group-id="4432836159-2">do</span><span class="w">
    </span><span class="p" data-group-id="4432836159-3">[</span><span class="w">
      </span><span class="c1"># ...</span><span class="w">
      </span><span class="p" data-group-id="4432836159-4">{</span><span class="ss">:nimble_publisher</span><span class="p">,</span><span class="w"> </span><span class="s">&quot;~&gt; 0.1.1&quot;</span><span class="p" data-group-id="4432836159-4">}</span><span class="w">
    </span><span class="p" data-group-id="4432836159-3">]</span><span class="w">
  </span><span class="k" data-group-id="4432836159-2">end</span><span class="w">

  </span><span class="c1">#...</span><span class="w">
</span><span class="k" data-group-id="4432836159-1">end</span></code></pre>
<p>
Add the <code class="inline">Blog</code> context and <code class="inline">Blog.Post</code> module:</p>
<pre><code class="makeup elixir"><span class="c1"># lib/my_blog/blog.ex</span><span class="w">
</span><span class="kd">defmodule</span><span class="w"> </span><span class="nc">MyBlog.Blog</span><span class="w"> </span><span class="k" data-group-id="9075695854-1">do</span><span class="w">
  </span><span class="kn">alias</span><span class="w"> </span><span class="nc">MyApp.Blog.Post</span><span class="w">

  </span><span class="kn">use</span><span class="w"> </span><span class="nc">NimblePublisher</span><span class="p">,</span><span class="w">
    </span><span class="ss">build</span><span class="p">:</span><span class="w"> </span><span class="nc">Post</span><span class="p">,</span><span class="w">
    </span><span class="ss">from</span><span class="p">:</span><span class="w"> </span><span class="nc">Application</span><span class="o">.</span><span class="n">app_dir</span><span class="p" data-group-id="9075695854-2">(</span><span class="ss">:my_blog</span><span class="p">,</span><span class="w"> </span><span class="s">&quot;priv/posts/**/*.md&quot;</span><span class="p" data-group-id="9075695854-2">)</span><span class="p">,</span><span class="w">
    </span><span class="ss">as</span><span class="p">:</span><span class="w"> </span><span class="ss">:posts</span><span class="p">,</span><span class="w">
    </span><span class="ss">highlighters</span><span class="p">:</span><span class="w"> </span><span class="p" data-group-id="9075695854-3">[</span><span class="ss">:makeup_elixir</span><span class="p">,</span><span class="w"> </span><span class="ss">:makeup_erlang</span><span class="p" data-group-id="9075695854-3">]</span><span class="w">

  </span><span class="na">@posts</span><span class="w"> </span><span class="nc">Enum</span><span class="o">.</span><span class="n">sort_by</span><span class="p" data-group-id="9075695854-4">(</span><span class="na">@posts</span><span class="p">,</span><span class="w"> </span><span class="o">&amp;</span><span class="w"> </span><span class="ni">&amp;1</span><span class="o">.</span><span class="n">date</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="9075695854-5">{</span><span class="ss">:desc</span><span class="p">,</span><span class="w"> </span><span class="nc">Date</span><span class="p" data-group-id="9075695854-5">}</span><span class="p" data-group-id="9075695854-4">)</span><span class="w">

  </span><span class="kd">def</span><span class="w"> </span><span class="nf">list_posts</span><span class="p">,</span><span class="w"> </span><span class="ss">do</span><span class="p">:</span><span class="w"> </span><span class="na">@posts</span><span class="w">

  </span><span class="na">@tags</span><span class="w"> </span><span class="na">@posts</span><span class="w"> </span><span class="o">|&gt;</span><span class="w"> </span><span class="nc">Enum</span><span class="o">.</span><span class="n">flat_map</span><span class="p" data-group-id="9075695854-6">(</span><span class="o">&amp;</span><span class="w"> </span><span class="ni">&amp;1</span><span class="o">.</span><span class="n">tags</span><span class="p" data-group-id="9075695854-6">)</span><span class="w"> </span><span class="o">|&gt;</span><span class="w"> </span><span class="nc">Enum</span><span class="o">.</span><span class="n">uniq</span><span class="p" data-group-id="9075695854-7">(</span><span class="p" data-group-id="9075695854-7">)</span><span class="w"> </span><span class="o">|&gt;</span><span class="w"> </span><span class="nc">Enum</span><span class="o">.</span><span class="n">sort</span><span class="p" data-group-id="9075695854-8">(</span><span class="p" data-group-id="9075695854-8">)</span><span class="w">

  </span><span class="kd">def</span><span class="w"> </span><span class="nf">list_tags</span><span class="p">,</span><span class="w"> </span><span class="ss">do</span><span class="p">:</span><span class="w"> </span><span class="na">@tags</span><span class="w">

  </span><span class="kd">defmodule</span><span class="w"> </span><span class="nc">NotFoundError</span><span class="w"> </span><span class="k" data-group-id="9075695854-9">do</span><span class="w">
    </span><span class="kd">defexception</span><span class="w"> </span><span class="p" data-group-id="9075695854-10">[</span><span class="ss">:message</span><span class="p">,</span><span class="w"> </span><span class="ss">plug_status</span><span class="p">:</span><span class="w"> </span><span class="mi">404</span><span class="p" data-group-id="9075695854-10">]</span><span class="w">
  </span><span class="k" data-group-id="9075695854-9">end</span><span class="w">

  </span><span class="kd">def</span><span class="w"> </span><span class="nf">get_post_by_id!</span><span class="p" data-group-id="9075695854-11">(</span><span class="n">id</span><span class="p" data-group-id="9075695854-11">)</span><span class="w"> </span><span class="k" data-group-id="9075695854-12">do</span><span class="w">
    </span><span class="nc">Enum</span><span class="o">.</span><span class="n">find</span><span class="p" data-group-id="9075695854-13">(</span><span class="n">list_posts</span><span class="p" data-group-id="9075695854-14">(</span><span class="p" data-group-id="9075695854-14">)</span><span class="p">,</span><span class="w"> </span><span class="o">&amp;</span><span class="p" data-group-id="9075695854-15">(</span><span class="ni">&amp;1</span><span class="o">.</span><span class="n">id</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="n">id</span><span class="p" data-group-id="9075695854-15">)</span><span class="p" data-group-id="9075695854-13">)</span><span class="w"> </span><span class="o">||</span><span class="w">
      </span><span class="k">raise</span><span class="w"> </span><span class="nc">NotFoundError</span><span class="p">,</span><span class="w"> </span><span class="s">&quot;post with id=</span><span class="si" data-group-id="9075695854-16">#{</span><span class="n">id</span><span class="si" data-group-id="9075695854-16">}</span><span class="s"> not found&quot;</span><span class="w">
  </span><span class="k" data-group-id="9075695854-12">end</span><span class="w">

  </span><span class="kd">def</span><span class="w"> </span><span class="nf">list_posts_by_tag!</span><span class="p" data-group-id="9075695854-17">(</span><span class="n">tag</span><span class="p" data-group-id="9075695854-17">)</span><span class="w"> </span><span class="k" data-group-id="9075695854-18">do</span><span class="w">
    </span><span class="k">case</span><span class="w"> </span><span class="nc">Enum</span><span class="o">.</span><span class="n">filter</span><span class="p" data-group-id="9075695854-19">(</span><span class="n">list_posts</span><span class="p" data-group-id="9075695854-20">(</span><span class="p" data-group-id="9075695854-20">)</span><span class="p">,</span><span class="w"> </span><span class="o">&amp;</span><span class="p" data-group-id="9075695854-21">(</span><span class="n">tag</span><span class="w"> </span><span class="ow">in</span><span class="w"> </span><span class="ni">&amp;1</span><span class="o">.</span><span class="n">tags</span><span class="p" data-group-id="9075695854-21">)</span><span class="p" data-group-id="9075695854-19">)</span><span class="w"> </span><span class="k" data-group-id="9075695854-22">do</span><span class="w">
      </span><span class="p" data-group-id="9075695854-23">[</span><span class="p" data-group-id="9075695854-23">]</span><span class="w"> </span><span class="o">-&gt;</span><span class="w"> </span><span class="k">raise</span><span class="w"> </span><span class="nc">NotFoundError</span><span class="p">,</span><span class="w"> </span><span class="s">&quot;posts with tag=</span><span class="si" data-group-id="9075695854-24">#{</span><span class="n">tag</span><span class="si" data-group-id="9075695854-24">}</span><span class="s"> not found&quot;</span><span class="w">
      </span><span class="n">posts</span><span class="w"> </span><span class="o">-&gt;</span><span class="w"> </span><span class="n">posts</span><span class="w">
    </span><span class="k" data-group-id="9075695854-22">end</span><span class="w">
  </span><span class="k" data-group-id="9075695854-18">end</span><span class="w">
</span><span class="k" data-group-id="9075695854-1">end</span></code></pre>
<pre><code class="makeup elixir"><span class="c1"># lib/my_blog/blog/post.ex</span><span class="w">
</span><span class="kd">defmodule</span><span class="w"> </span><span class="nc">MyBlog.Blog.Post</span><span class="w"> </span><span class="k" data-group-id="6324937695-1">do</span><span class="w">
  </span><span class="na">@enforce_keys</span><span class="w"> </span><span class="p" data-group-id="6324937695-2">[</span><span class="ss">:id</span><span class="p">,</span><span class="w"> </span><span class="ss">:title</span><span class="p">,</span><span class="w"> </span><span class="ss">:body</span><span class="p">,</span><span class="w"> </span><span class="ss">:description</span><span class="p">,</span><span class="w"> </span><span class="ss">:tags</span><span class="p">,</span><span class="w"> </span><span class="ss">:date</span><span class="p" data-group-id="6324937695-2">]</span><span class="w">
  </span><span class="kd">defstruct</span><span class="w"> </span><span class="p" data-group-id="6324937695-3">[</span><span class="ss">:id</span><span class="p">,</span><span class="w"> </span><span class="ss">:title</span><span class="p">,</span><span class="w"> </span><span class="ss">:body</span><span class="p">,</span><span class="w"> </span><span class="ss">:description</span><span class="p">,</span><span class="w"> </span><span class="ss">:tags</span><span class="p">,</span><span class="w"> </span><span class="ss">:date</span><span class="p" data-group-id="6324937695-3">]</span><span class="w">

  </span><span class="kd">def</span><span class="w"> </span><span class="nf">build</span><span class="p" data-group-id="6324937695-4">(</span><span class="n">filename</span><span class="p">,</span><span class="w"> </span><span class="n">attrs</span><span class="p">,</span><span class="w"> </span><span class="n">body</span><span class="p" data-group-id="6324937695-4">)</span><span class="w"> </span><span class="k" data-group-id="6324937695-5">do</span><span class="w">
    </span><span class="p" data-group-id="6324937695-6">[</span><span class="n">year</span><span class="p">,</span><span class="w"> </span><span class="n">month_day_id</span><span class="p" data-group-id="6324937695-6">]</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">filename</span><span class="w"> </span><span class="o">|&gt;</span><span class="w"> </span><span class="nc">Path</span><span class="o">.</span><span class="n">rootname</span><span class="p" data-group-id="6324937695-7">(</span><span class="p" data-group-id="6324937695-7">)</span><span class="w"> </span><span class="o">|&gt;</span><span class="w"> </span><span class="nc">Path</span><span class="o">.</span><span class="n">split</span><span class="p" data-group-id="6324937695-8">(</span><span class="p" data-group-id="6324937695-8">)</span><span class="w"> </span><span class="o">|&gt;</span><span class="w"> </span><span class="nc">Enum</span><span class="o">.</span><span class="n">take</span><span class="p" data-group-id="6324937695-9">(</span><span class="o">-</span><span class="mi">2</span><span class="p" data-group-id="6324937695-9">)</span><span class="w">
    </span><span class="p" data-group-id="6324937695-10">[</span><span class="n">month</span><span class="p">,</span><span class="w"> </span><span class="n">day</span><span class="p">,</span><span class="w"> </span><span class="n">id</span><span class="p" data-group-id="6324937695-10">]</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">String</span><span class="o">.</span><span class="n">split</span><span class="p" data-group-id="6324937695-11">(</span><span class="n">month_day_id</span><span class="p">,</span><span class="w"> </span><span class="s">&quot;-&quot;</span><span class="p">,</span><span class="w"> </span><span class="ss">parts</span><span class="p">:</span><span class="w"> </span><span class="mi">3</span><span class="p" data-group-id="6324937695-11">)</span><span class="w">
    </span><span class="n">date</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">Date</span><span class="o">.</span><span class="n">from_iso8601!</span><span class="p" data-group-id="6324937695-12">(</span><span class="s">&quot;</span><span class="si" data-group-id="6324937695-13">#{</span><span class="n">year</span><span class="si" data-group-id="6324937695-13">}</span><span class="s">-</span><span class="si" data-group-id="6324937695-14">#{</span><span class="n">month</span><span class="si" data-group-id="6324937695-14">}</span><span class="s">-</span><span class="si" data-group-id="6324937695-15">#{</span><span class="n">day</span><span class="si" data-group-id="6324937695-15">}</span><span class="s">&quot;</span><span class="p" data-group-id="6324937695-12">)</span><span class="w">
    </span><span class="n">struct!</span><span class="p" data-group-id="6324937695-16">(</span><span class="bp">__MODULE__</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="6324937695-17">[</span><span class="ss">id</span><span class="p">:</span><span class="w"> </span><span class="n">id</span><span class="p">,</span><span class="w"> </span><span class="ss">date</span><span class="p">:</span><span class="w"> </span><span class="n">date</span><span class="p">,</span><span class="w"> </span><span class="ss">body</span><span class="p">:</span><span class="w"> </span><span class="n">body</span><span class="p" data-group-id="6324937695-17">]</span><span class="w"> </span><span class="o">++</span><span class="w"> </span><span class="nc">Map</span><span class="o">.</span><span class="n">to_list</span><span class="p" data-group-id="6324937695-18">(</span><span class="n">attrs</span><span class="p" data-group-id="6324937695-18">)</span><span class="p" data-group-id="6324937695-16">)</span><span class="w">
  </span><span class="k" data-group-id="6324937695-5">end</span><span class="w">
</span><span class="k" data-group-id="6324937695-1">end</span></code></pre>
<p>
Create a new blog post:</p>
<pre><code class="makeup elixir"><span class="c1"># priv/posts/2023/01-01-hello-world.md</span><span class="w">
</span><span class="p" data-group-id="9745660204-1">%{</span><span class="w">
  </span><span class="ss">title</span><span class="p">:</span><span class="w"> </span><span class="s">&quot;Hello world&quot;</span><span class="p">,</span><span class="w">
  </span><span class="ss">tags</span><span class="p">:</span><span class="w"> </span><span class="sx">~w(hello)</span><span class="p">,</span><span class="w">
  </span><span class="ss">description</span><span class="p">:</span><span class="w"> </span><span class="s">&quot;This is my first blog post&quot;</span><span class="w">
</span><span class="p" data-group-id="9745660204-1">}</span><span class="w">
</span><span class="o">--</span><span class="o">-</span><span class="w">
</span><span class="nc">Hello</span><span class="w"> </span><span class="n">world!</span></code></pre>
<p>
Set up the routes:</p>
<pre><code class="makeup elixir"><span class="c1"># lib/my_blog_web/router.ex</span><span class="w">
</span><span class="kd">defmodule</span><span class="w"> </span><span class="nc">MyBlogWeb.Router</span><span class="w"> </span><span class="k" data-group-id="2374406297-1">do</span><span class="w">
  </span><span class="kn">use</span><span class="w"> </span><span class="nc">MyBlogWeb</span><span class="p">,</span><span class="w"> </span><span class="ss">:router</span><span class="w">

  </span><span class="c1"># ...</span><span class="w">

  </span><span class="n">scope</span><span class="w"> </span><span class="s">&quot;/&quot;</span><span class="p">,</span><span class="w"> </span><span class="nc">MyBlogWeb</span><span class="w"> </span><span class="k" data-group-id="2374406297-2">do</span><span class="w">
    </span><span class="n">pipe_through</span><span class="w"> </span><span class="ss">:browser</span><span class="w">

    </span><span class="n">live</span><span class="w"> </span><span class="s">&quot;/&quot;</span><span class="p">,</span><span class="w"> </span><span class="nc">BlogLive</span><span class="p">,</span><span class="w"> </span><span class="ss">:index</span><span class="w">
    </span><span class="n">live</span><span class="w"> </span><span class="s">&quot;/tags/:tag&quot;</span><span class="p">,</span><span class="w"> </span><span class="nc">BlogLive</span><span class="p">,</span><span class="w"> </span><span class="ss">:index</span><span class="w">
    </span><span class="n">live</span><span class="w"> </span><span class="s">&quot;/posts/:slug&quot;</span><span class="p">,</span><span class="w"> </span><span class="nc">BlogLive</span><span class="p">,</span><span class="w"> </span><span class="ss">:show</span><span class="w">
  </span><span class="k" data-group-id="2374406297-2">end</span><span class="w">

  </span><span class="c1"># ...</span><span class="w">
</span><span class="k" data-group-id="2374406297-1">end</span></code></pre>
<p>
Set up the <code class="inline">BlogLive</code> module and the templates:</p>
<pre><code class="makeup elixir"><span class="c1"># lib/my_blog_web/live/blog_live.ex</span><span class="w">
</span><span class="kd">defmodule</span><span class="w"> </span><span class="nc">MyBlogWeb.BlogLive</span><span class="w"> </span><span class="k" data-group-id="4692386820-1">do</span><span class="w">
  </span><span class="kn">use</span><span class="w"> </span><span class="nc">MyBlogWeb</span><span class="p">,</span><span class="w"> </span><span class="ss">:live_view</span><span class="w">

  </span><span class="n">embed_templates</span><span class="w"> </span><span class="s">&quot;blog_live/*&quot;</span><span class="w">

  </span><span class="na">@impl</span><span class="w"> </span><span class="no">true</span><span class="w">
  </span><span class="kd">def</span><span class="w"> </span><span class="nf">mount</span><span class="p" data-group-id="4692386820-2">(</span><span class="c">_params</span><span class="p">,</span><span class="w"> </span><span class="c">_session</span><span class="p">,</span><span class="w"> </span><span class="n">socket</span><span class="p" data-group-id="4692386820-2">)</span><span class="w"> </span><span class="k" data-group-id="4692386820-3">do</span><span class="w">
    </span><span class="p" data-group-id="4692386820-4">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="n">socket</span><span class="p" data-group-id="4692386820-4">}</span><span class="w">
  </span><span class="k" data-group-id="4692386820-3">end</span><span class="w">

  </span><span class="na">@impl</span><span class="w"> </span><span class="no">true</span><span class="w">
  </span><span class="kd">def</span><span class="w"> </span><span class="nf">handle_params</span><span class="p" data-group-id="4692386820-5">(</span><span class="n">params</span><span class="p">,</span><span class="w"> </span><span class="c">_url</span><span class="p">,</span><span class="w"> </span><span class="n">socket</span><span class="p" data-group-id="4692386820-5">)</span><span class="w"> </span><span class="k" data-group-id="4692386820-6">do</span><span class="w">
    </span><span class="p" data-group-id="4692386820-7">{</span><span class="ss">:noreply</span><span class="p">,</span><span class="w"> </span><span class="n">apply_action</span><span class="p" data-group-id="4692386820-8">(</span><span class="n">socket</span><span class="p">,</span><span class="w"> </span><span class="n">socket</span><span class="o">.</span><span class="n">assigns</span><span class="o">.</span><span class="n">live_action</span><span class="p">,</span><span class="w"> </span><span class="n">params</span><span class="p" data-group-id="4692386820-8">)</span><span class="p" data-group-id="4692386820-7">}</span><span class="w">
  </span><span class="k" data-group-id="4692386820-6">end</span><span class="w">

  </span><span class="kd">defp</span><span class="w"> </span><span class="nf">apply_action</span><span class="p" data-group-id="4692386820-9">(</span><span class="n">socket</span><span class="p">,</span><span class="w"> </span><span class="ss">:index</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="4692386820-10">%{</span><span class="s">&quot;tag&quot;</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="n">tag</span><span class="p" data-group-id="4692386820-10">}</span><span class="p" data-group-id="4692386820-9">)</span><span class="w"> </span><span class="k" data-group-id="4692386820-11">do</span><span class="w">
    </span><span class="n">socket</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">assign</span><span class="p" data-group-id="4692386820-12">(</span><span class="ss">:page_title</span><span class="p">,</span><span class="w"> </span><span class="n">tag</span><span class="p" data-group-id="4692386820-12">)</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">assign</span><span class="p" data-group-id="4692386820-13">(</span><span class="ss">:posts</span><span class="p">,</span><span class="w"> </span><span class="nc">MyBlog.Blog</span><span class="o">.</span><span class="n">list_posts_by_tag!</span><span class="p" data-group-id="4692386820-14">(</span><span class="n">tag</span><span class="p" data-group-id="4692386820-14">)</span><span class="p" data-group-id="4692386820-13">)</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">assign</span><span class="p" data-group-id="4692386820-15">(</span><span class="ss">:tag</span><span class="p">,</span><span class="w"> </span><span class="n">tag</span><span class="p" data-group-id="4692386820-15">)</span><span class="w">
  </span><span class="k" data-group-id="4692386820-11">end</span><span class="w">

  </span><span class="kd">defp</span><span class="w"> </span><span class="nf">apply_action</span><span class="p" data-group-id="4692386820-16">(</span><span class="n">socket</span><span class="p">,</span><span class="w"> </span><span class="ss">:index</span><span class="p">,</span><span class="w"> </span><span class="c">_params</span><span class="p" data-group-id="4692386820-16">)</span><span class="w"> </span><span class="k" data-group-id="4692386820-17">do</span><span class="w">
    </span><span class="n">assign</span><span class="p" data-group-id="4692386820-18">(</span><span class="n">socket</span><span class="p">,</span><span class="w"> </span><span class="ss">:posts</span><span class="p">,</span><span class="w"> </span><span class="nc">MyBlog.Blog</span><span class="o">.</span><span class="n">list_posts</span><span class="p" data-group-id="4692386820-19">(</span><span class="p" data-group-id="4692386820-19">)</span><span class="p" data-group-id="4692386820-18">)</span><span class="w">
  </span><span class="k" data-group-id="4692386820-17">end</span><span class="w">

  </span><span class="kd">defp</span><span class="w"> </span><span class="nf">apply_action</span><span class="p" data-group-id="4692386820-20">(</span><span class="n">socket</span><span class="p">,</span><span class="w"> </span><span class="ss">:show</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="4692386820-21">%{</span><span class="s">&quot;slug&quot;</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="n">slug</span><span class="p" data-group-id="4692386820-21">}</span><span class="p" data-group-id="4692386820-20">)</span><span class="w"> </span><span class="k" data-group-id="4692386820-22">do</span><span class="w">
    </span><span class="n">post</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">MyBlog.Blog</span><span class="o">.</span><span class="n">get_post_by_id!</span><span class="p" data-group-id="4692386820-23">(</span><span class="n">slug</span><span class="p" data-group-id="4692386820-23">)</span><span class="w">

    </span><span class="n">socket</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">assign</span><span class="p" data-group-id="4692386820-24">(</span><span class="ss">:page_title</span><span class="p">,</span><span class="w"> </span><span class="n">post</span><span class="o">.</span><span class="n">title</span><span class="p" data-group-id="4692386820-24">)</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="n">assign</span><span class="p" data-group-id="4692386820-25">(</span><span class="ss">:post</span><span class="p">,</span><span class="w"> </span><span class="n">post</span><span class="p" data-group-id="4692386820-25">)</span><span class="w">
  </span><span class="k" data-group-id="4692386820-22">end</span><span class="w">

  </span><span class="na">@impl</span><span class="w"> </span><span class="no">true</span><span class="w">
  </span><span class="kd">def</span><span class="w"> </span><span class="nf">render</span><span class="p" data-group-id="4692386820-26">(</span><span class="n">assigns</span><span class="p" data-group-id="4692386820-26">)</span><span class="w"> </span><span class="k" data-group-id="4692386820-27">do</span><span class="w">
    </span><span class="n">apply</span><span class="p" data-group-id="4692386820-28">(</span><span class="bp">__MODULE__</span><span class="p">,</span><span class="w"> </span><span class="n">assigns</span><span class="o">.</span><span class="n">live_action</span><span class="p">,</span><span class="w"> </span><span class="p" data-group-id="4692386820-29">[</span><span class="n">assigns</span><span class="p" data-group-id="4692386820-29">]</span><span class="p" data-group-id="4692386820-28">)</span><span class="w">
  </span><span class="k" data-group-id="4692386820-27">end</span><span class="w">
</span><span class="k" data-group-id="4692386820-1">end</span></code></pre>
<pre><code class="makeup elixir"><span class="c1"># lib/my_blog_web/live/blog_live/index.html.eex</span><span class="w">
</span><span class="o">&lt;</span><span class="n">ul</span><span class="o">&gt;</span><span class="w">
  </span><span class="o">&lt;</span><span class="n">li</span><span class="w"> </span><span class="ss">:for</span><span class="o">=</span><span class="p" data-group-id="7248470371-1">{</span><span class="n">post</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="na">@posts</span><span class="p" data-group-id="7248470371-1">}</span><span class="w"> </span><span class="n">class</span><span class="o">=</span><span class="s">&quot;py-4&quot;</span><span class="o">&gt;</span><span class="w">
    </span><span class="o">&lt;</span><span class="n">article</span><span class="w"> </span><span class="n">class</span><span class="o">=</span><span class="s">&quot;space-y-2 xl:grid xl:grid-cols-4 xl:items-baseline xl:space-y-0&quot;</span><span class="o">&gt;</span><span class="w">
      </span><span class="o">&lt;</span><span class="n">dl</span><span class="o">&gt;</span><span class="w">
        </span><span class="o">&lt;</span><span class="n">dt</span><span class="w"> </span><span class="n">class</span><span class="o">=</span><span class="s">&quot;sr-only&quot;</span><span class="o">&gt;</span><span class="nc">Published</span><span class="w"> </span><span class="n">on</span><span class="o">&lt;</span><span class="o">/</span><span class="n">dt</span><span class="o">&gt;</span><span class="w">
        </span><span class="o">&lt;</span><span class="n">dd</span><span class="w"> </span><span class="n">class</span><span class="o">=</span><span class="s">&quot;text-base font-medium leading-6 text-gray-500&quot;</span><span class="o">&gt;</span><span class="w">
          </span><span class="o">&lt;</span><span class="n">time</span><span class="w"> </span><span class="n">datetime</span><span class="o">=</span><span class="p" data-group-id="7248470371-2">{</span><span class="n">post</span><span class="o">.</span><span class="n">date</span><span class="p" data-group-id="7248470371-2">}</span><span class="o">&gt;</span><span class="p" data-group-id="7248470371-3">{</span><span class="n">post</span><span class="o">.</span><span class="n">date</span><span class="p" data-group-id="7248470371-3">}</span><span class="o">&lt;</span><span class="o">/</span><span class="n">time</span><span class="o">&gt;</span><span class="w">
        </span><span class="o">&lt;</span><span class="o">/</span><span class="n">dd</span><span class="o">&gt;</span><span class="w">
      </span><span class="o">&lt;</span><span class="o">/</span><span class="n">dl</span><span class="o">&gt;</span><span class="w">
      </span><span class="o">&lt;</span><span class="n">div</span><span class="w"> </span><span class="n">class</span><span class="o">=</span><span class="s">&quot;space-y-3 xl:col-span-3&quot;</span><span class="o">&gt;</span><span class="w">
        </span><span class="o">&lt;</span><span class="n">div</span><span class="o">&gt;</span><span class="w">
          </span><span class="o">&lt;</span><span class="n">h3</span><span class="w"> </span><span class="n">class</span><span class="o">=</span><span class="s">&quot;text-2xl font-bold leading-8 tracking-tight&quot;</span><span class="o">&gt;</span><span class="w">
            </span><span class="o">&lt;</span><span class="o">.</span><span class="n">link</span><span class="w"> </span><span class="n">navigate</span><span class="o">=</span><span class="p" data-group-id="7248470371-4">{</span><span class="sx">~p&quot;/posts/</span><span class="si" data-group-id="7248470371-5">#{</span><span class="n">post</span><span class="o">.</span><span class="n">id</span><span class="si" data-group-id="7248470371-5">}</span><span class="sx">&quot;</span><span class="p" data-group-id="7248470371-4">}</span><span class="w"> </span><span class="n">class</span><span class="o">=</span><span class="s">&quot;text-gray-900&quot;</span><span class="o">&gt;</span><span class="w">
              </span><span class="p" data-group-id="7248470371-6">{</span><span class="n">post</span><span class="o">.</span><span class="n">title</span><span class="p" data-group-id="7248470371-6">}</span><span class="w">
            </span><span class="o">&lt;</span><span class="o">/</span><span class="o">.</span><span class="n">link</span><span class="o">&gt;</span><span class="w">
          </span><span class="o">&lt;</span><span class="o">/</span><span class="n">h3</span><span class="o">&gt;</span><span class="w">
          </span><span class="o">&lt;</span><span class="n">ul</span><span class="w"> </span><span class="n">class</span><span class="o">=</span><span class="s">&quot;flex flex-wrap&quot;</span><span class="o">&gt;</span><span class="w">
            </span><span class="o">&lt;</span><span class="n">li</span><span class="w"> </span><span class="ss">:for</span><span class="o">=</span><span class="p" data-group-id="7248470371-7">{</span><span class="n">tag</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="n">post</span><span class="o">.</span><span class="n">tags</span><span class="p" data-group-id="7248470371-7">}</span><span class="w"> </span><span class="n">class</span><span class="o">=</span><span class="s">&quot;mr-3&quot;</span><span class="o">&gt;</span><span class="w">
              </span><span class="o">&lt;</span><span class="o">.</span><span class="n">link</span><span class="w"> </span><span class="n">navigate</span><span class="o">=</span><span class="p" data-group-id="7248470371-8">{</span><span class="sx">~p&quot;/tags/</span><span class="si" data-group-id="7248470371-9">#{</span><span class="n">tag</span><span class="si" data-group-id="7248470371-9">}</span><span class="sx">&quot;</span><span class="p" data-group-id="7248470371-8">}</span><span class="w"> </span><span class="n">class</span><span class="o">=</span><span class="s">&quot;text-sm font-light uppercase text-black/50&quot;</span><span class="o">&gt;</span><span class="w">
                </span><span class="p" data-group-id="7248470371-10">{</span><span class="n">tag</span><span class="p" data-group-id="7248470371-10">}</span><span class="w">
              </span><span class="o">&lt;</span><span class="o">/</span><span class="o">.</span><span class="n">link</span><span class="o">&gt;</span><span class="w">
            </span><span class="o">&lt;</span><span class="o">/</span><span class="n">li</span><span class="o">&gt;</span><span class="w">
          </span><span class="o">&lt;</span><span class="o">/</span><span class="n">ul</span><span class="o">&gt;</span><span class="w">
        </span><span class="o">&lt;</span><span class="o">/</span><span class="n">div</span><span class="o">&gt;</span><span class="w">
        </span><span class="o">&lt;</span><span class="n">div</span><span class="w"> </span><span class="n">class</span><span class="o">=</span><span class="s">&quot;prose max-w-none text-black/50&quot;</span><span class="o">&gt;</span><span class="w">
          </span><span class="p" data-group-id="7248470371-11">{</span><span class="n">post</span><span class="o">.</span><span class="n">description</span><span class="p" data-group-id="7248470371-11">}</span><span class="w">
        </span><span class="o">&lt;</span><span class="o">/</span><span class="n">div</span><span class="o">&gt;</span><span class="w">
      </span><span class="o">&lt;</span><span class="o">/</span><span class="n">div</span><span class="o">&gt;</span><span class="w">
    </span><span class="o">&lt;</span><span class="o">/</span><span class="n">article</span><span class="o">&gt;</span><span class="w">
  </span><span class="o">&lt;</span><span class="o">/</span><span class="n">li</span><span class="o">&gt;</span><span class="w">
</span><span class="o">&lt;</span><span class="o">/</span><span class="n">ul</span><span class="o">&gt;</span></code></pre>
<pre><code class="makeup elixir"><span class="c1"># lib/my_blog/live/blog_live/show.html.eex</span><span class="w">
</span><span class="o">&lt;</span><span class="n">div</span><span class="w"> </span><span class="n">class</span><span class="o">=</span><span class="s">&quot;mx-auto px-4 max-w-4xl&quot;</span><span class="o">&gt;</span><span class="w">
  </span><span class="o">&lt;</span><span class="n">div</span><span class="w"> </span><span class="n">class</span><span class="o">=</span><span class="s">&quot;py-5 text-center&quot;</span><span class="o">&gt;</span><span class="w">
    </span><span class="o">&lt;</span><span class="n">p</span><span class="w"> </span><span class="n">class</span><span class="o">=</span><span class="s">&quot;text-sm&quot;</span><span class="o">&gt;</span><span class="w">
      </span><span class="o">&lt;</span><span class="n">dt</span><span class="w"> </span><span class="n">class</span><span class="o">=</span><span class="s">&quot;sr-only&quot;</span><span class="o">&gt;</span><span class="nc">Published</span><span class="w"> </span><span class="n">on</span><span class="o">&lt;</span><span class="o">/</span><span class="n">dt</span><span class="o">&gt;</span><span class="w">
      </span><span class="o">&lt;</span><span class="n">dd</span><span class="w"> </span><span class="n">class</span><span class="o">=</span><span class="s">&quot;text-base font-medium leading-6 text-white/50&quot;</span><span class="o">&gt;</span><span class="w">
        </span><span class="o">&lt;</span><span class="n">time</span><span class="w"> </span><span class="n">datetime</span><span class="o">=</span><span class="p" data-group-id="5389603853-1">{</span><span class="na">@post</span><span class="o">.</span><span class="n">date</span><span class="p" data-group-id="5389603853-1">}</span><span class="o">&gt;</span><span class="p" data-group-id="5389603853-2">{</span><span class="na">@post</span><span class="o">.</span><span class="n">date</span><span class="p" data-group-id="5389603853-2">}</span><span class="o">&lt;</span><span class="o">/</span><span class="n">time</span><span class="o">&gt;</span><span class="w">
      </span><span class="o">&lt;</span><span class="o">/</span><span class="n">dd</span><span class="o">&gt;</span><span class="w">
    </span><span class="o">&lt;</span><span class="o">/</span><span class="n">p</span><span class="o">&gt;</span><span class="w">
    </span><span class="o">&lt;</span><span class="n">h1</span><span class="w"> </span><span class="n">class</span><span class="o">=</span><span class="s">&quot;text-5xl font-extrabold&quot;</span><span class="o">&gt;</span><span class="p" data-group-id="5389603853-3">{</span><span class="na">@post</span><span class="o">.</span><span class="n">title</span><span class="p" data-group-id="5389603853-3">}</span><span class="o">&lt;</span><span class="o">/</span><span class="n">h1</span><span class="o">&gt;</span><span class="w">
  </span><span class="o">&lt;</span><span class="o">/</span><span class="n">div</span><span class="o">&gt;</span><span class="w">

  </span><span class="o">&lt;</span><span class="n">div</span><span class="w"> </span><span class="n">class</span><span class="o">=</span><span class="s">&quot;prose py-8 max-w-4xl text-lg&quot;</span><span class="o">&gt;</span><span class="w">
    </span><span class="p" data-group-id="5389603853-4">{</span><span class="n">raw</span><span class="w"> </span><span class="na">@post</span><span class="o">.</span><span class="n">body</span><span class="p" data-group-id="5389603853-4">}</span><span class="w">
  </span><span class="o">&lt;</span><span class="o">/</span><span class="n">div</span><span class="o">&gt;</span><span class="w">
</span><span class="o">&lt;</span><span class="o">/</span><span class="n">div</span><span class="o">&gt;</span></code></pre>
<p>
Finally, update live reload to improve the dev experience (otherwise you have to restart the server constantly):</p>
<pre><code class="makeup elixir"><span class="n">config</span><span class="w"> </span><span class="ss">:my_blog</span><span class="p">,</span><span class="w"> </span><span class="nc">MyBlogWeb.Endpoint</span><span class="p">,</span><span class="w">
  </span><span class="ss">live_reload</span><span class="p">:</span><span class="w"> </span><span class="p" data-group-id="1978468796-1">[</span><span class="w">
    </span><span class="ss">patterns</span><span class="p">:</span><span class="w"> </span><span class="p" data-group-id="1978468796-2">[</span><span class="w">
      </span><span class="c1"># ...</span><span class="w">
      </span><span class="sr">~r&quot;posts/*/.*(md)$&quot;</span><span class="w">
    </span><span class="p" data-group-id="1978468796-2">]</span><span class="w">
  </span><span class="p" data-group-id="1978468796-1">]</span></code></pre>
<p>
Now run <code class="inline">mix deps.get</code>, <code class="inline">mix phx.server</code>, and visit <a href="http://localhost:4000/">http://localhost:4000/</a>.</p>
<p>
That was easy!</p>
<p>
The only thing you are missing is to style it which seems to always be what takes the longest when starting a new blog 😅</p>

      ]]>
    </content>
  </entry>
  
</feed>
