<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Kensio Software Freelance Development Blog | Kensio Software</title><link>https://kensiosoftware.co.uk/blog/</link><description>Notes from Hugh Grigg on freelance software development: TypeScript and Node.js packages, AWS and serverless architecture, testing practice, and write-ups of…</description><generator>Hugo</generator><language>en-gb</language><managingEditor>hugh@kensiosoftware.co.uk (Hugh Grigg)</managingEditor><webMaster>hugh@kensiosoftware.co.uk (Hugh Grigg)</webMaster><copyright>© 2026 Kensio Software</copyright><lastBuildDate>Mon, 27 Jul 2026 12:26:15 +0000</lastBuildDate><atom:link href="https://kensiosoftware.co.uk/blog/index.xml" rel="self" type="application/rss+xml"/><item><title>@kensio/colophon social meta image npm package</title><link>https://kensiosoftware.co.uk/blog/npm-kensio-colophon-social-meta-images-package/</link><pubDate>Mon, 27 Jul 2026 12:26:15 +0000</pubDate><author>hugh@kensiosoftware.co.uk (Hugh Grigg)</author><guid>https://kensiosoftware.co.uk/blog/npm-kensio-colophon-social-meta-images-package/</guid><description>The @kensio/colophon package generates social meta images for the posts of a static site from each post’s own frontmatter, including syntax highlighted code…</description><content:encoded><![CDATA[<p>The
<a href="https://github.com/KensioSoftware/colophon" title="Social meta image generation npm package">@kensio/colophon</a>
package generates the social meta images for the posts of a static website, driven by each post&rsquo;s
own frontmatter.</p>
<p>Every post wants an <code>og:image</code> and a <code>twitter:image</code>, because that is what a link to it looks like
when someone shares it. Making those by hand is quite a lot of extra work per post, and the posts
that don&rsquo;t get it end up as blank grey rectangles in the preview.</p>
<p>The usual answer is a script in the site repository. That works, but the layout, the fonts and the
colours end up welded to the one site it was written for, and the next site starts again from
nothing.</p>
<p><em>Colophon</em> takes the description of the image from the post and the branding from configuration, so
the same package can generate images for sites with different branding. The package name comes from
the printer&rsquo;s <em>colophon</em>, which is an emblem a publisher stamps on a finished work.</p>
<h2 id="the-image-is-described-in-the-post">The image is described in the post</h2>
<p>A post declares what its image should look like, including the image template:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nn">---</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">title</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;@kensio/yulin v0.34.2 adds simulated IAM&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">slug</span><span class="p">:</span><span class="w"> </span><span class="l">npm-kensio-yulin-simulated-iam</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">meta_img_props</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">template</span><span class="p">:</span><span class="w"> </span><span class="l">banner</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">title</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;@kensio/yulin&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">subtitle</span><span class="p">:</span><span class="w"> </span><span class="l">Simulated IAM</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">version</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;0.34.2&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nn">---</span><span class="w">
</span></span></span></code></pre></div><p>Running the CLI over a content tree writes one PNG per output size next to each post that declares
those props, named after the post&rsquo;s slug:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">colophon content --config colophon.config.ts
</span></span></code></pre></div><p>That gives <code>npm-kensio-yulin-simulated-iam-og.png</code> and <code>npm-kensio-yulin-simulated-iam-square.png</code>
in the post&rsquo;s own directory. Files that already exist are skipped unless the run is passed
<code>--overwrite</code>, so regenerating a whole site only recreates the images that are actually missing.</p>
<p>The default output set is one 1200x630 landscape and one 1200x1200 square, which between them cover
<code>og:image</code> and both Twitter card types. Other sizes are available as presets, or as any
<code>{ name, width, height }</code> you define.</p>
<p>There are three templates so far. <code>banner</code> is a left-aligned title with an optional version,
subtitle, corner badge and footer. <code>card</code> is a centred title and subtitle with nothing else. <code>code</code>
renders a snippet.</p>
<h2 id="code-images">Code images</h2>
<p>The <code>code</code> template takes the snippet from the frontmatter and highlights it with real editor theme
colours, using <a href="https://shiki.style/" title="Syntax highlighter">Shiki</a> for the grammars and themes:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nn">---</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">title</span><span class="p">:</span><span class="w"> </span><span class="l">eslint changed TypeScript files only</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">slug</span><span class="p">:</span><span class="w"> </span><span class="l">eslint-changed-ts-files-only</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">meta_img_props</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">template</span><span class="p">:</span><span class="w"> </span><span class="l">code</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">language</span><span class="p">:</span><span class="w"> </span><span class="l">bash</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">code</span><span class="p">:</span><span class="w"> </span><span class="p">|</span><span class="sd">
</span></span></span><span class="line"><span class="cl"><span class="sd">    mapfile -t CHANGED_TS &lt; &lt;(
</span></span></span><span class="line"><span class="cl"><span class="sd">      git diff origin/main --name-only \
</span></span></span><span class="line"><span class="cl"><span class="sd">        | grep &#39;\.ts&#39;
</span></span></span><span class="line"><span class="cl"><span class="sd">    )</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nn">---</span><span class="w">
</span></span></span></code></pre></div><p>Fitting arbitrary code into a fixed image is the part that needed the most thought. Colophon
measures the longest line and the line count against a monospace grid, then picks the largest font
size that fits on both axes.</p>
<p>Those bounds are fractions of the image width rather than absolute sizes, because that is what a
feed scales a share image to. Sizing against the height instead would render the same snippet in a
landscape image at half the size of its square counterpart.</p>
<p>A snippet that still doesn&rsquo;t fit at the smallest allowed size is truncated with an ellipsis rather
than shrunk into something illegible. Since a finished image gives no sign that it lost anything,
Colophon reports it:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">colophon: content/post/index.md: code snippet does not fit the 1200x630 image at
</span></span><span class="line"><span class="cl">a legible size: 4 of 13 lines dropped. Shorten the sample, or lower
</span></span><span class="line"><span class="cl">code.minFontScale to fit it in smaller.
</span></span></code></pre></div><p>The practical budget at the default settings is around nine lines of about sixty characters for a
landscape image. Snippets written for that will render identically at every size.</p>
<h2 id="branding-comes-from-config">Branding comes from config</h2>
<p>Colours, background, fonts, footer and badge are configuration rather than anything read from the
site&rsquo;s own stylesheet:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-typescript" data-lang="typescript"><span class="line"><span class="cl"><span class="kr">import</span> <span class="p">{</span> <span class="nx">defineConfig</span> <span class="p">}</span> <span class="kr">from</span> <span class="s2">&#34;@kensio/colophon&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kr">export</span> <span class="k">default</span> <span class="nx">defineConfig</span><span class="p">({</span>
</span></span><span class="line"><span class="cl">  <span class="nx">colors</span><span class="o">:</span> <span class="p">{</span> <span class="nx">brand</span><span class="o">:</span> <span class="s2">&#34;#2563eb&#34;</span><span class="p">,</span> <span class="nx">brandDark</span><span class="o">:</span> <span class="s2">&#34;#1e3a8a&#34;</span><span class="p">,</span> <span class="nx">brandWarm</span><span class="o">:</span> <span class="s2">&#34;#f59e0b&#34;</span> <span class="p">},</span>
</span></span><span class="line"><span class="cl">  <span class="nx">footer</span><span class="o">:</span> <span class="s2">&#34;example.com&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nx">badge</span><span class="o">:</span> <span class="p">{</span> <span class="nx">text</span><span class="o">:</span> <span class="s2">&#34;npm&#34;</span> <span class="p">},</span>
</span></span><span class="line"><span class="cl"><span class="p">});</span>
</span></span></code></pre></div><p>Custom templates go in the same config. A template is a name and a <code>render</code> function returning SVG,
and anything registered there merges over the built-ins.</p>
<p>For programmatic use, <code>renderMetaImages</code> takes props and config and returns the rendered bytes with
no filesystem involved, while <code>generate</code> ties walking a content tree, rendering and writing together
in the way the CLI does.</p>
<h2 id="this-site-runs-on-it">This site runs on it</h2>
<p>The images on this blog are generated by a script that is now about twenty lines, most of which is
logging what it wrote:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-typescript" data-lang="typescript"><span class="line"><span class="cl"><span class="kr">import</span> <span class="p">{</span> <span class="nx">generate</span> <span class="p">}</span> <span class="kr">from</span> <span class="s2">&#34;@kensio/colophon&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kr">import</span> <span class="p">{</span> <span class="nx">colophonConfig</span> <span class="p">}</span> <span class="kr">from</span> <span class="s2">&#34;./image/colophon-config.js&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="kr">import</span> <span class="p">{</span> <span class="nx">repoPath</span> <span class="p">}</span> <span class="kr">from</span> <span class="s2">&#34;./util/path.js&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">results</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">generate</span><span class="p">({</span>
</span></span><span class="line"><span class="cl">  <span class="nx">contentDir</span>: <span class="kt">repoPath</span><span class="p">(</span><span class="s2">&#34;content&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">  <span class="nx">config</span>: <span class="kt">colophonConfig</span><span class="p">(),</span>
</span></span><span class="line"><span class="cl">  <span class="nx">overwrite</span>: <span class="kt">process.argv.slice</span><span class="p">(</span><span class="mi">2</span><span class="p">).</span><span class="nx">includes</span><span class="p">(</span><span class="s2">&#34;--overwrite&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl"><span class="p">});</span>
</span></span></code></pre></div><p>The config which that script passes to <code>colophon</code> compiles the theme&rsquo;s own Sass variables and reads
the brand colour back out of the result. That means the meta images track the site palette instead
of holding a second copy of it.</p>
<p>That replaced a bespoke image generator of a little over three hundred lines living in this
repository, which did the same job for this site alone.</p>
<h2 id="what-it-doesnt-do">What it doesn&rsquo;t do</h2>
<p>Colophon renders SVG to PNG from a small set of layouts. It&rsquo;s not a design tool. If you need a
genuinely different layout, then you can write a new custom template.</p>
<p>It also assumes a static site with frontmatter in markdown files. The render core has no filesystem
concerns at all, so it can be driven from something else, but the walker and the CLI are built
around that arrangement.</p>
<p>The package is on npm as
<a href="https://www.npmjs.com/package/@kensio/colophon" title="Social meta images from frontmatter">@kensio/colophon</a>.</p>
]]></content:encoded><enclosure url="https://kensiosoftware.co.uk/blog/npm-kensio-colophon-social-meta-images-package/npm-kensio-colophon-social-meta-images-package-og.png" type="image/png"/><category>TypeScript &amp; JavaScript</category><category>Node.js</category></item><item><title>@kensio/yulin v0.34.2 adds simulated Lambda</title><link>https://kensiosoftware.co.uk/blog/npm-kensio-yulin-simulated-lambda/</link><pubDate>Fri, 24 Jul 2026 18:59:55 +0000</pubDate><author>hugh@kensiosoftware.co.uk (Hugh Grigg)</author><guid>https://kensiosoftware.co.uk/blog/npm-kensio-yulin-simulated-lambda/</guid><description>@kensio/yulin package v0.34.2 adds simulated AWS Lambda, so you can create and invoke functions in-process, including from CloudFormation templates and real…</description><content:encoded><![CDATA[<p>Version <code>v0.34.2</code> of the <code>@kensio/yulin</code> package adds a
<a href="https://yulinsim.dev/services/lambda/" title="Simulated Lambda docs">simulated Lambda service</a> for local
development and isolated testing.</p>
<p>Functions are created and invoked entirely in-process and in memory, with no containers and no real
AWS infrastructure.</p>
<p>The simulator supports <code>CreateFunctionCommand</code>, <code>GetFunctionCommand</code> and <code>InvokeCommand</code>, including
the <code>RequestResponse</code>, <code>Event</code> and <code>DryRun</code> invocation types.</p>
<p>It also supports <code>AWS::Lambda::Function</code> resources when deploying CloudFormation or CDK templates
into the simulated AWS.</p>
<p>Function code can come from three places: a real in-process handler function, zip archive bytes on
<code>Code.ZipFile</code>, or a zip object stored in sim S3.</p>
<p>The quickest of those is passing an ordinary function from your own process as the function code.
It can be stepped through in a debugger and can close over local test state:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-typescript" data-lang="typescript"><span class="line"><span class="cl"><span class="kr">import</span> <span class="p">{</span> <span class="nx">CreateFunctionCommand</span><span class="p">,</span> <span class="nx">InvokeCommand</span> <span class="p">}</span> <span class="kr">from</span> <span class="s2">&#34;@aws-sdk/client-lambda&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kr">import</span> <span class="p">{</span> <span class="nx">SimAws</span> <span class="p">}</span> <span class="kr">from</span> <span class="s2">&#34;@kensio/yulin&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="kr">import</span> <span class="p">{</span> <span class="nx">makeLambdaZipFileInput</span> <span class="p">}</span> <span class="kr">from</span> <span class="s2">&#34;@kensio/yulin/lambda&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">simAws</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">SimAws</span><span class="p">();</span>
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">lambda</span> <span class="o">=</span> <span class="nx">simAws</span><span class="p">.</span><span class="nx">lambda</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">await</span> <span class="nx">lambda</span><span class="p">.</span><span class="nx">createFunction</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="k">new</span> <span class="nx">CreateFunctionCommand</span><span class="p">({</span>
</span></span><span class="line"><span class="cl">    <span class="nx">FunctionName</span><span class="o">:</span> <span class="s2">&#34;example-greeter&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nx">Role</span><span class="o">:</span> <span class="s2">&#34;arn:aws:iam::123456789012:role/ExampleGreeterRole&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nx">Code</span><span class="o">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="nx">ZipFile</span>: <span class="kt">makeLambdaZipFileInput</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="p">(</span><span class="nx">event</span><span class="o">:</span> <span class="p">{</span> <span class="nx">name</span>: <span class="kt">string</span> <span class="p">})</span> <span class="o">=&gt;</span> <span class="sb">`Hello </span><span class="si">${</span><span class="nx">event</span><span class="p">.</span><span class="nx">name</span><span class="si">}</span><span class="sb">`</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="p">),</span>
</span></span><span class="line"><span class="cl">    <span class="p">},</span>
</span></span><span class="line"><span class="cl">  <span class="p">}),</span>
</span></span><span class="line"><span class="cl"><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">output</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">lambda</span><span class="p">.</span><span class="nx">invoke</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="k">new</span> <span class="nx">InvokeCommand</span><span class="p">({</span>
</span></span><span class="line"><span class="cl">    <span class="nx">FunctionName</span><span class="o">:</span> <span class="s2">&#34;example-greeter&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nx">Payload</span>: <span class="kt">JSON.stringify</span><span class="p">({</span> <span class="nx">name</span><span class="o">:</span> <span class="s2">&#34;Yulin&#34;</span> <span class="p">}),</span>
</span></span><span class="line"><span class="cl">  <span class="p">}),</span>
</span></span><span class="line"><span class="cl"><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">Buffer</span><span class="p">.</span><span class="kr">from</span><span class="p">(</span><span class="nx">output</span><span class="p">.</span><span class="nx">Payload</span><span class="p">).</span><span class="nx">toString</span><span class="p">());</span>
</span></span></code></pre></div><p><code>makeLambdaZipFileInput</code> produces something the SDK-shaped <code>Code.ZipFile</code> input accepts, so the
create call keeps the shape it has against the real AWS SDK.</p>
<p>Handlers take the real <code>(event, context, callback)</code> signature, and typed handlers written against
the <code>aws-lambda</code> typings package can be passed in unchanged.</p>
<p>Creating a function requires an execution <code>Role</code> ARN, as on real AWS. A new function starts
<code>Pending</code> and becomes <code>Active</code> in the background, so <code>simAws.backgroundTasksComplete()</code> is there
for tests that assert on the state.</p>
<p>A handler that throws is reported AWS-style, with <code>FunctionError: &quot;Unhandled&quot;</code> and an error
document payload, rather than the invoke call itself throwing.</p>
<p>Real zip archives work too. <code>makeLambdaCodeZip</code> builds real zip bytes from a source string or from
a files map keyed by archive path.</p>
<p>That archive runs in a Node.js <code>vm</code> context with cold start semantics, so the module is imported
once on first invocation and its state stays warm across invocations afterwards.</p>
<p>The runtime provides <code>@aws-sdk/*</code> packages the way the real Node.js runtime does, without them
being bundled into the archive. Clients constructed inside function code are routed into the same
simulated AWS environment.</p>
<p>Calls the handler makes run as the function&rsquo;s execution role, so simulated IAM authorizes them just
as a real execution role would. Given an execution role allowed to read from <code>example-bucket</code>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-typescript" data-lang="typescript"><span class="line"><span class="cl"><span class="kr">import</span> <span class="p">{</span> <span class="nx">CreateFunctionCommand</span><span class="p">,</span> <span class="nx">InvokeCommand</span> <span class="p">}</span> <span class="kr">from</span> <span class="s2">&#34;@aws-sdk/client-lambda&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kr">import</span> <span class="p">{</span> <span class="nx">SimAws</span> <span class="p">}</span> <span class="kr">from</span> <span class="s2">&#34;@kensio/yulin&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="kr">import</span> <span class="p">{</span> <span class="nx">makeLambdaCodeZip</span> <span class="p">}</span> <span class="kr">from</span> <span class="s2">&#34;@kensio/yulin/lambda&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">simAws</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">SimAws</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">await</span> <span class="nx">simAws</span><span class="p">.</span><span class="nx">lambda</span><span class="p">().</span><span class="nx">createFunction</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="k">new</span> <span class="nx">CreateFunctionCommand</span><span class="p">({</span>
</span></span><span class="line"><span class="cl">    <span class="nx">FunctionName</span><span class="o">:</span> <span class="s2">&#34;example-reader&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nx">Role</span>: <span class="kt">readerRole.Arn</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nx">Handler</span><span class="o">:</span> <span class="s2">&#34;index.handler&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nx">Code</span><span class="o">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="nx">ZipFile</span>: <span class="kt">makeLambdaCodeZip</span><span class="p">(</span><span class="sb">`
</span></span></span><span class="line"><span class="cl"><span class="sb">        const { S3Client, GetObjectCommand } = require(&#34;@aws-sdk/client-s3&#34;);
</span></span></span><span class="line"><span class="cl"><span class="sb">        const s3Client = new S3Client({});
</span></span></span><span class="line"><span class="cl"><span class="sb">        exports.handler = async (event) =&gt; {
</span></span></span><span class="line"><span class="cl"><span class="sb">          const output = await s3Client.send(
</span></span></span><span class="line"><span class="cl"><span class="sb">            new GetObjectCommand({
</span></span></span><span class="line"><span class="cl"><span class="sb">              Bucket: &#34;example-bucket&#34;,
</span></span></span><span class="line"><span class="cl"><span class="sb">              Key: event.objectKey,
</span></span></span><span class="line"><span class="cl"><span class="sb">            }),
</span></span></span><span class="line"><span class="cl"><span class="sb">          );
</span></span></span><span class="line"><span class="cl"><span class="sb">          return await output.Body.transformToString();
</span></span></span><span class="line"><span class="cl"><span class="sb">        };
</span></span></span><span class="line"><span class="cl"><span class="sb">      `</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">    <span class="p">},</span>
</span></span><span class="line"><span class="cl">  <span class="p">}),</span>
</span></span><span class="line"><span class="cl"><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">output</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">simAws</span><span class="p">.</span><span class="nx">lambda</span><span class="p">().</span><span class="nx">invoke</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="k">new</span> <span class="nx">InvokeCommand</span><span class="p">({</span>
</span></span><span class="line"><span class="cl">    <span class="nx">FunctionName</span><span class="o">:</span> <span class="s2">&#34;example-reader&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nx">Payload</span>: <span class="kt">JSON.stringify</span><span class="p">({</span> <span class="nx">objectKey</span><span class="o">:</span> <span class="s2">&#34;public/config.json&#34;</span> <span class="p">}),</span>
</span></span><span class="line"><span class="cl">  <span class="p">}),</span>
</span></span><span class="line"><span class="cl"><span class="p">);</span>
</span></span></code></pre></div><p>The <code>require(&quot;@aws-sdk/client-s3&quot;)</code> there resolves to the real package, with its clients pointed at
the simulated AWS rather than at anything on the network.</p>
<p>If the execution role is not allowed to make that read, sim IAM denies it and the invocation
reports the denial as a function error, in the same way a real execution role denial surfaces
inside the handler.</p>
<p>Sim CloudFormation can create functions as well, so they can come from the same template as the
rest of the infrastructure:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;Resources&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;ExampleGreeterFunction&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;Type&#34;</span><span class="p">:</span> <span class="s2">&#34;AWS::Lambda::Function&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;Properties&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nt">&#34;FunctionName&#34;</span><span class="p">:</span> <span class="s2">&#34;example-greeter&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="nt">&#34;Role&#34;</span><span class="p">:</span> <span class="s2">&#34;arn:aws:iam::123456789012:role/ExampleGreeterRole&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="nt">&#34;Handler&#34;</span><span class="p">:</span> <span class="s2">&#34;index.handler&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="nt">&#34;Runtime&#34;</span><span class="p">:</span> <span class="s2">&#34;nodejs22.x&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="nt">&#34;Code&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">          <span class="nt">&#34;ZipFile&#34;</span><span class="p">:</span> <span class="s2">&#34;exports.handler = async (event) =&gt; &#39;Hello &#39; + event.name;&#34;</span>
</span></span><span class="line"><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="cl">      <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>Inline <code>ZipFile</code> template source is packaged and run in the vm runtime, as if it had been zipped and
passed to <code>CreateFunctionCommand</code>.</p>
<p>The <code>Role</code> is usually an <code>Fn::GetAtt</code> reference to a same-stack <code>AWS::IAM::Role</code>, which resolves to
that Role&rsquo;s ARN. <code>Ref</code> on the function returns its name and <code>Fn::GetAtt</code> supports <code>Arn</code>.</p>
<p>Deploy-time bindings can back a template function with a real in-process handler instead of its
template code, which is the CloudFormation counterpart of <code>makeLambdaZipFileInput</code>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-typescript" data-lang="typescript"><span class="line"><span class="cl"><span class="kr">import</span> <span class="p">{</span> <span class="nx">SimAws</span> <span class="p">}</span> <span class="kr">from</span> <span class="s2">&#34;@kensio/yulin&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">simAws</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">SimAws</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">await</span> <span class="nx">simAws</span><span class="p">.</span><span class="nx">cloudFormation</span><span class="p">().</span><span class="nx">deployTemplate</span><span class="p">({</span>
</span></span><span class="line"><span class="cl">  <span class="nx">stackName</span><span class="o">:</span> <span class="s2">&#34;example-greeter-stack&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nx">template</span>: <span class="kt">exampleTemplate</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nx">bindings</span><span class="o">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">    <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="nx">logicalId</span><span class="o">:</span> <span class="s2">&#34;ExampleGreeterFunction&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nx">handler</span><span class="o">:</span> <span class="p">(</span><span class="nx">event</span><span class="o">:</span> <span class="p">{</span> <span class="nx">name</span>: <span class="kt">string</span> <span class="p">})</span><span class="o">:</span> <span class="kt">string</span> <span class="o">=&gt;</span> <span class="sb">`Hello </span><span class="si">${</span><span class="nx">event</span><span class="p">.</span><span class="nx">name</span><span class="si">}</span><span class="sb">`</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="p">},</span>
</span></span><span class="line"><span class="cl">  <span class="p">],</span>
</span></span><span class="line"><span class="cl"><span class="p">});</span>
</span></span></code></pre></div><p>A bound function can leave out its template <code>Code</code> and <code>Handler</code> entirely, and other functions in
the same template keep running their template code.</p>
<p>Bindings can target a function by <code>logicalId</code>, <code>functionName</code>, <code>arn</code> or full <code>cdkPath</code>, and the
logical ID also matches a CDK construct ID from <code>aws:cdk:path</code> metadata.</p>
<p>Sim Lambda covers what isolated tests and local development need rather than full Lambda parity.
The <a href="https://github.com/KensioSoftware/yulin" title="Yulin simulated AWS on GitHub">Lambda tests in the repository</a>
are the clearest picture of what is actually covered.</p>
<p><code>UpdateFunctionCode</code>, <code>DeleteFunction</code> and function listing are not implemented yet, and versions,
aliases and qualifiers are not simulated.</p>
<p>The vm runtime runs CommonJS function code only, so ES module source fails with a hint rather than
running. Container image functions are not supported, as the simulator stays Docker-free, and
Layers are not simulated.</p>
<p><code>Environment.Variables</code> is not applied yet, and <code>Timeout</code> is recorded without interrupting the
handler.</p>
<p>The <code>vm</code> context is a namespacing convenience rather than a security boundary. Function code runs
in-process with the same trust as the test suite itself, so this is not a way to run untrusted
code.</p>
<p>The Lambda simulator integrates with the rest of Yulin, so S3, IAM, CloudFormation and CDK can all
participate in the same simulated environment for local development and system tests.</p>
<p>npm: <a href="https://www.npmjs.com/package/@kensio/yulin" title="Local AWS simulator npm package">https://www.npmjs.com/package/@kensio/yulin</a></p>
]]></content:encoded><enclosure url="https://kensiosoftware.co.uk/blog/npm-kensio-yulin-simulated-lambda/npm-kensio-yulin-simulated-lambda-og.png" type="image/png"/><category>AWS (Amazon Web Services)</category><category>TypeScript &amp; JavaScript</category><category>Node.js</category></item><item><title>@kensio/yulin v0.34.2 adds simulated IAM</title><link>https://kensiosoftware.co.uk/blog/npm-kensio-yulin-simulated-iam/</link><pubDate>Fri, 24 Jul 2026 18:09:56 +0000</pubDate><author>hugh@kensiosoftware.co.uk (Hugh Grigg)</author><guid>https://kensiosoftware.co.uk/blog/npm-kensio-yulin-simulated-iam/</guid><description>@kensio/yulin package v0.34.2 adds simulated AWS IAM, so you can create Roles, Users and Policies, and have other simulated services authorize their own…</description><content:encoded><![CDATA[<p>Version <code>v0.34.2</code> of the
<a href="https://www.npmjs.com/package/@kensio/yulin" title="Local AWS simulator npm package">@kensio/yulin</a>
package adds a <a href="https://yulinsim.dev/services/iam/" title="Simulated IAM docs">simulated IAM service</a> for
local development and isolated testing.</p>
<p>The simulator supports Roles with <code>CreateRoleCommand</code> and Users with <code>CreateUserCommand</code>, plus
inline policies with <code>PutRolePolicyCommand</code> and <code>PutUserPolicyCommand</code>.</p>
<p>Managed policies are created with <code>CreatePolicyCommand</code> and attached with <code>AttachRolePolicyCommand</code>.</p>
<p>Sim IAM evaluates those policies into allow/deny decisions. Simulated STS issues temporary Role
sessions with <code>AssumeRoleCommand</code>, after checking the Role&rsquo;s trust policy.</p>
<p>Creating a Role with an inline policy and authorizing an action against it looks much the same as
it does against the real AWS SDK:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-typescript" data-lang="typescript"><span class="line"><span class="cl"><span class="kr">import</span> <span class="p">{</span> <span class="nx">CreateRoleCommand</span><span class="p">,</span> <span class="nx">PutRolePolicyCommand</span> <span class="p">}</span> <span class="kr">from</span> <span class="s2">&#34;@aws-sdk/client-iam&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="kr">import</span> <span class="p">{</span> <span class="nx">SimAws</span> <span class="p">}</span> <span class="kr">from</span> <span class="s2">&#34;@kensio/yulin&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">simAws</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">SimAws</span><span class="p">();</span>
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">simIam</span> <span class="o">=</span> <span class="nx">simAws</span><span class="p">.</span><span class="nx">account</span><span class="p">(</span><span class="s2">&#34;123456789012&#34;</span><span class="p">).</span><span class="nx">iam</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">createRoleOutput</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">simIam</span><span class="p">.</span><span class="nx">createRole</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="k">new</span> <span class="nx">CreateRoleCommand</span><span class="p">({</span>
</span></span><span class="line"><span class="cl">    <span class="nx">RoleName</span><span class="o">:</span> <span class="s2">&#34;ExampleReaderRole&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nx">AssumeRolePolicyDocument</span>: <span class="kt">JSON.stringify</span><span class="p">({</span>
</span></span><span class="line"><span class="cl">      <span class="nx">Version</span><span class="o">:</span> <span class="s2">&#34;2012-10-17&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nx">Statement</span><span class="o">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nx">Effect</span><span class="o">:</span> <span class="s2">&#34;Allow&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="nx">Principal</span><span class="o">:</span> <span class="p">{</span> <span class="nx">AWS</span><span class="o">:</span> <span class="s2">&#34;arn:aws:iam::123456789012:root&#34;</span> <span class="p">},</span>
</span></span><span class="line"><span class="cl">        <span class="nx">Action</span><span class="o">:</span> <span class="s2">&#34;sts:AssumeRole&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="p">},</span>
</span></span><span class="line"><span class="cl">    <span class="p">}),</span>
</span></span><span class="line"><span class="cl">  <span class="p">}),</span>
</span></span><span class="line"><span class="cl"><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">await</span> <span class="nx">simIam</span><span class="p">.</span><span class="nx">putRolePolicy</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="k">new</span> <span class="nx">PutRolePolicyCommand</span><span class="p">({</span>
</span></span><span class="line"><span class="cl">    <span class="nx">RoleName</span><span class="o">:</span> <span class="s2">&#34;ExampleReaderRole&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nx">PolicyName</span><span class="o">:</span> <span class="s2">&#34;ReadExampleObjects&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nx">PolicyDocument</span>: <span class="kt">JSON.stringify</span><span class="p">({</span>
</span></span><span class="line"><span class="cl">      <span class="nx">Version</span><span class="o">:</span> <span class="s2">&#34;2012-10-17&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nx">Statement</span><span class="o">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">        <span class="p">{</span>
</span></span><span class="line"><span class="cl">          <span class="nx">Effect</span><span class="o">:</span> <span class="s2">&#34;Allow&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">          <span class="nx">Action</span><span class="o">:</span> <span class="s2">&#34;s3:GetObject&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">          <span class="nx">Resource</span><span class="o">:</span> <span class="s2">&#34;arn:aws:s3:::example-bucket/*&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="p">},</span>
</span></span><span class="line"><span class="cl">        <span class="p">{</span>
</span></span><span class="line"><span class="cl">          <span class="nx">Effect</span><span class="o">:</span> <span class="s2">&#34;Deny&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">          <span class="nx">Action</span><span class="o">:</span> <span class="s2">&#34;s3:GetObject&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">          <span class="nx">Resource</span><span class="o">:</span> <span class="s2">&#34;arn:aws:s3:::example-bucket/protected/config.json&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="p">},</span>
</span></span><span class="line"><span class="cl">      <span class="p">],</span>
</span></span><span class="line"><span class="cl">    <span class="p">}),</span>
</span></span><span class="line"><span class="cl">  <span class="p">}),</span>
</span></span><span class="line"><span class="cl"><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">decision</span> <span class="o">=</span> <span class="nx">simIam</span><span class="p">.</span><span class="nx">authorize</span><span class="p">({</span>
</span></span><span class="line"><span class="cl">  <span class="nx">action</span><span class="o">:</span> <span class="s2">&#34;s3:GetObject&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nx">resource</span><span class="o">:</span> <span class="s2">&#34;arn:aws:s3:::example-bucket/public/config.json&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nx">caller</span><span class="o">:</span> <span class="p">{</span> <span class="nx">kind</span><span class="o">:</span> <span class="s2">&#34;arn&#34;</span><span class="p">,</span> <span class="nx">arn</span>: <span class="kt">createRoleOutput.Role.Arn</span> <span class="p">},</span>
</span></span><span class="line"><span class="cl"><span class="p">});</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">decision</span><span class="p">.</span><span class="nx">isAllowed</span><span class="p">);</span>
</span></span></code></pre></div><p>The <code>authorize()</code> method returns a decision object rather than throwing, so a test can assert on
why a request was allowed or denied.</p>
<p>The decision exposes <code>value</code> as <code>&quot;Allow&quot;</code>, <code>&quot;ExplicitDeny&quot;</code> or <code>&quot;ImplicitDeny&quot;</code>, along with the
statements that matched and the resolved caller.</p>
<p>An explicit <code>Deny</code> in any evaluated policy wins. Otherwise a matching <code>Allow</code> in an identity or
resource policy allows the request, and anything else is an implicit deny.</p>
<p>The part that matters most for system tests is that the other simulated services authorize their
own actions through sim IAM.</p>
<p>That covers S3, Route53, DynamoDB, ACM, CloudFront and Lambda actions, including <code>s3:GetObject</code>,
<code>route53:CreateHostedZone</code>, <code>dynamodb:PutItem</code> and <code>lambda:InvokeFunction</code>.</p>
<p>Sim IAM and sim CloudFormation authorize their own control plane commands as well, so
<code>iam:CreateRole</code> and <code>cloudformation:CreateStack</code> can be denied for a caller that is not allowed to
make them. Those commands take an optional caller:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-typescript" data-lang="typescript"><span class="line"><span class="cl"><span class="kr">import</span> <span class="p">{</span> <span class="nx">GetObjectCommand</span> <span class="p">}</span> <span class="kr">from</span> <span class="s2">&#34;@aws-sdk/client-s3&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">simS3</span> <span class="o">=</span> <span class="nx">simAws</span><span class="p">.</span><span class="nx">account</span><span class="p">(</span><span class="s2">&#34;123456789012&#34;</span><span class="p">).</span><span class="nx">region</span><span class="p">(</span><span class="s2">&#34;eu-west-2&#34;</span><span class="p">).</span><span class="nx">s3</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">output</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">simS3</span><span class="p">.</span><span class="nx">getObject</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="k">new</span> <span class="nx">GetObjectCommand</span><span class="p">({</span>
</span></span><span class="line"><span class="cl">    <span class="nx">Bucket</span><span class="o">:</span> <span class="s2">&#34;example-bucket&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nx">Key</span><span class="o">:</span> <span class="s2">&#34;public/config.json&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="p">}),</span>
</span></span><span class="line"><span class="cl">  <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nx">caller</span><span class="o">:</span> <span class="p">{</span> <span class="nx">kind</span><span class="o">:</span> <span class="s2">&#34;arn&#34;</span><span class="p">,</span> <span class="nx">arn</span>: <span class="kt">createRoleOutput.Role.Arn</span> <span class="p">},</span>
</span></span><span class="line"><span class="cl">  <span class="p">},</span>
</span></span><span class="line"><span class="cl"><span class="p">);</span>
</span></span></code></pre></div><p>Reading <code>protected/config.json</code> with that same caller throws an AWS-like access denied error with a
<code>403</code> status code, before the simulated S3 service looks the Object up at all.</p>
<p>Omitting the caller defaults to the Account root, which is allowed within its own Account, so
existing tests that never mention IAM keep working.</p>
<p>S3 Bucket policies are supplied to sim IAM as resource policies for the request, so a Bucket policy
can allow or deny a read that identity policies say nothing about.</p>
<p>Policy conditions are supported for the <code>StringEquals</code>, <code>StringLike</code> and <code>NumericLessThanEquals</code>
operators, including the <code>ForAllValues:</code> and <code>ForAnyValue:</code> set variants of the string operators.
Condition values such as S3 Object tags are supplied by the service handling the request.</p>
<p>Access keys created with <code>CreateAccessKeyCommand</code> are registered with the simulated Account, so a
caller can be given as credentials rather than an ARN.</p>
<p>Those credentials are authenticated before any policies are evaluated, as are the temporary session
credentials returned by simulated STS.</p>
<p>Sim CloudFormation can create IAM resources too, so Roles and policies can come from the same
template as the rest of the infrastructure:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;Resources&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;ExampleRole&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;Type&#34;</span><span class="p">:</span> <span class="s2">&#34;AWS::IAM::Role&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;Properties&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nt">&#34;RoleName&#34;</span><span class="p">:</span> <span class="s2">&#34;ExampleReaderRole&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="nt">&#34;AssumeRolePolicyDocument&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">          <span class="nt">&#34;Version&#34;</span><span class="p">:</span> <span class="s2">&#34;2012-10-17&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">          <span class="nt">&#34;Statement&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="nt">&#34;Effect&#34;</span><span class="p">:</span> <span class="s2">&#34;Allow&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="nt">&#34;Principal&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">              <span class="nt">&#34;Service&#34;</span><span class="p">:</span> <span class="s2">&#34;lambda.amazonaws.com&#34;</span>
</span></span><span class="line"><span class="cl">            <span class="p">},</span>
</span></span><span class="line"><span class="cl">            <span class="nt">&#34;Action&#34;</span><span class="p">:</span> <span class="s2">&#34;sts:AssumeRole&#34;</span>
</span></span><span class="line"><span class="cl">          <span class="p">}</span>
</span></span><span class="line"><span class="cl">        <span class="p">},</span>
</span></span><span class="line"><span class="cl">        <span class="nt">&#34;Policies&#34;</span><span class="p">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">          <span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="nt">&#34;PolicyName&#34;</span><span class="p">:</span> <span class="s2">&#34;ReadExampleObjects&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="nt">&#34;PolicyDocument&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">              <span class="nt">&#34;Version&#34;</span><span class="p">:</span> <span class="s2">&#34;2012-10-17&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">              <span class="nt">&#34;Statement&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">                <span class="nt">&#34;Effect&#34;</span><span class="p">:</span> <span class="s2">&#34;Allow&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                <span class="nt">&#34;Action&#34;</span><span class="p">:</span> <span class="s2">&#34;s3:GetObject&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                <span class="nt">&#34;Resource&#34;</span><span class="p">:</span> <span class="s2">&#34;arn:aws:s3:::example-bucket/*&#34;</span>
</span></span><span class="line"><span class="cl">              <span class="p">}</span>
</span></span><span class="line"><span class="cl">            <span class="p">}</span>
</span></span><span class="line"><span class="cl">          <span class="p">}</span>
</span></span><span class="line"><span class="cl">        <span class="p">]</span>
</span></span><span class="line"><span class="cl">      <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p><code>AWS::IAM::Role</code>, <code>AWS::IAM::ManagedPolicy</code> and <code>AWS::IAM::Policy</code> are supported.</p>
<p>An <code>AWS::IAM::Policy</code> puts its document onto each Role named in <code>Roles</code> as an inline policy, which
is the shape CDK grants such as <code>bucket.grantRead(fn)</code> synthesise into.</p>
<p>IAM Users and Groups are not simulated as policy principals, so a template naming them fails
deployment rather than dropping the grant.</p>
<p>Sim IAM implements the policy behaviour that multi-service tests most commonly need rather than
full IAM parity.</p>
<p>Permissions boundaries, session policies and service control policies are not evaluated. Managed
policies have a single version, and deleting or detaching Roles, Users, policies and access keys is
not supported yet.</p>
<p>IAM is usually the last part of an AWS system to get any testing. Tests mock the SDK call away, so
the policies themselves are only exercised once everything is deployed.</p>
<p>That is a slow way to find out that a Role is missing a permission, or that a wildcard was broader
than it looked.</p>
<p>This is why I think IAM is one of the more useful things to simulate. A test can deploy the real
template, act as a specific Role, and assert that the read it should not be allowed to make is
actually denied, in the same run as the rest of the system tests.</p>
<p>The IAM simulator integrates with the rest of Yulin, so S3, Route53, DynamoDB, CloudFront, Lambda,
CloudFormation and CDK can all participate in the same simulated environment for local development
and system tests.</p>
]]></content:encoded><enclosure url="https://kensiosoftware.co.uk/blog/npm-kensio-yulin-simulated-iam/npm-kensio-yulin-simulated-iam-og.png" type="image/png"/><category>AWS (Amazon Web Services)</category><category>TypeScript &amp; JavaScript</category><category>Node.js</category></item><item><title>Video: Simulating AWS locally for website hosting with Yulin</title><link>https://kensiosoftware.co.uk/blog/video-simulate-aws-local-website-host-yulin/</link><pubDate>Mon, 06 Jul 2026 13:23:20 +0000</pubDate><author>hugh@kensiosoftware.co.uk (Hugh Grigg)</author><guid>https://kensiosoftware.co.uk/blog/video-simulate-aws-local-website-host-yulin/</guid><description>Demo video for the @kensio/yulin npm package: Simulating AWS locally for website hosting with Yulin</description><content:encoded><![CDATA[<div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;">
			<iframe allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share; fullscreen" loading="eager" referrerpolicy="strict-origin-when-cross-origin" src="https://www.youtube.com/embed/hkhi5uzc03w?autoplay=0&amp;controls=1&amp;end=0&amp;loop=0&amp;mute=0&amp;start=0" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" title="YouTube video"></iframe>
		</div>

<p>How to use the Yulin local AWS simulator for Node.js to simulate website hosting, including Route53,
S3, CloudFront and CloudFront Functions, provisioned via simulated CloudFormation.</p>
<p>YouTube: <a href="https://www.youtube.com/watch?v=hkhi5uzc03w">https://www.youtube.com/watch?v=hkhi5uzc03w</a></p>
<h2 id="transcript">Transcript</h2>
<p>In this video, I&rsquo;m going to show you how to simulate AWS for static website hosting entirely on
local host, including Route 53, ACM, S3, Cloudfront, and CloudFront Functions.</p>
<p>So this is the demo website project. It&rsquo;s an Astro website built with the Astro framework. Let&rsquo;s see
what the website looks like.</p>
<p>Here we have the <code>astro dev</code> command, which starts a local development server. I&rsquo;ll run that and we
get the Astro dev server on local host port 4321. Let&rsquo;s open it.</p>
<p>Here&rsquo;s the demo website running on local host 4321. It&rsquo;s just the official Astro blog theme with a
few small modifications. So that&rsquo;s our starting point.</p>
<p>Now, that&rsquo;s fine if you just want to view the website locally. But what if you&rsquo;re deploying it to
AWS. A production deployment usually involves several AWS services. For example, you might be using
CDK, and your stack could look something like this.</p>
<p>This is a real CDK stack for deploying a static website. Lets walk through it. First, we have a
domain name. This isn&rsquo;t a real registered domain. It&rsquo;s just for the demo, yulin-demo-website.com.</p>
<p>We have a Route 53 hosted zone for that domain, an ACM certificate so that we can use HTTPS instead
of HTTP and an S3 bucket containing the static website files.</p>
<p>We also have two CloudFront Functions, which we&rsquo;ll come back to shortly, and then we have a
CloudFront distribution, which actually serves the website over the internet using our domain name
and ACM certificate. It also associates those two CloudFront Functions.</p>
<p>Next is an S3 bucket deployment. This is a CDK feature that automatically copies files from a local
directory into the S3 buckets during deployment, which is very useful for static websites.</p>
<p>Finally, we have a Route 53 alias record. Alias records are an AWS specific feature that let you
point the root of a domain directly at an AWS service, such as a CloudFront Distribution. We also
have the IPv6 equivalent.</p>
<p>The only other things in the stack are three outputs which expose useful values, like the website
URL, and we&rsquo;ll use those later.</p>
<p>If we look back at the bucket deployment, it&rsquo;s expecting to copy files from a local <code>dist</code>
directory. Right now, that directory doesn&rsquo;t exist because it&rsquo;s produced by the Astro build step. So
let&rsquo;s run the build.</p>
<p>Cool. That builds the website instead of just serving it. We can now see the generated HTML files in
the <code>dist</code> directory, which means the bucket deployment now has something to upload.</p>
<p>The other thing we need is a CloudFormation template. CDK generates this for us and it&rsquo;s what we use
to deploy to either real AWS or our local simulation. I&rsquo;ll run <code>pnpm exec cdk synth</code> that prints the
template to the terminal, but it also creates a new <code>cdk.out</code> directory containing the deployment
artefacts.</p>
<p>Here&rsquo;s the JSON template. It&rsquo;s the CloudFormation version of our CDK stack and describes all of the
resources and their configuration.</p>
<p>Now that we have that, we&rsquo;re ready to simulate the entire stack locally with Yulin. Below the Astro
scripts, we have another script called <code>local-dev.mts</code>. Let&rsquo;s open that.</p>
<p>This creates a simulated AWS environment and starts a local host server that serves it. It then uses
simulated CloudFormation to deploy the JSON template that we just generated. We point it at the
template inside <code>cdk.out</code> and it deploys all of those resources into the simulated AWS environment.
Finally, it reads the website URL output from the stack, asks the local server for the local host
equivalent and logs that URL for convenience. So let&rsquo;s run it.</p>
<p>Cool. That&rsquo;s deployed the entire stack into simulated AWS. The first thing you&rsquo;ll notice is that we
have our real domain name with the Route 53 hosted zone attached as a prefix to the
<code>sim-aws.localhost</code> domain. This works not only for Route 53 hosted zones, but also for CloudFront
distribution domain names, and S3 website host names.</p>
<p>Let&rsquo;s open the simulated website. The browser is now visiting our simulated Route 53 domain, and the
website looks identical to the one served by Astro&rsquo;s development server.</p>
<p>If we open the developer tools and reload the page, let&rsquo;s have a look at the response headers,
you&rsquo;ll notice a few extra security headers, such as <code>Referrer-Policy</code> and <code>X-Content-Type-Options</code>.</p>
<p>Now let&rsquo;s compare that with the Astro development server. If we reload the same page there, those
headers aren&rsquo;t present, so where are they coming from?</p>
<p>Back in our project, they&rsquo;re being added by the viewer response CloudFront Function that we saw
earlier. A viewer response function intercepts responses on their way back to the client. In our CDK
stack, this resource brings the CloudFront Function source code into the deployment and then
associates it with the CloudFront distribution as a viewer response function.</p>
<p>Let&rsquo;s look at the function itself. The source code lives in <code>src/cff/viewer-response</code>. As you can
see, this is the code that&rsquo;s adding those security headers before the response is sent back to the
browser. So that&rsquo;s one example of functionality. We&rsquo;re able to simulate and test locally.</p>
<p>We also have another CloudFront Function, the viewer request function. It&rsquo;s associated with the
distribution as a viewer request function and its source code lives in <code>viewer-request</code>. Let&rsquo;s open
that one. This function does a bit more.</p>
<p>One thing it handles is trailing slash or pretty URL routing. A URL ending with a slash is actually
served by an <code>index.html</code> object in S3 behind the scenes. For example, <code>blog/first-post/</code> is really
serving <code>blog/first-post/index.html</code>. That means visitors only need to browse to <code>blog/first-post/</code>
and this CloudFront Function transparently maps that to the correct file. That behaviour is the same
in both real AWS and our local simulation.</p>
<p>The function also handles legacy redirects. For example, maybe this site originally served blog
posts under <code>/posts</code> but later we decided <code>/blog</code> was a better URL structure. We can keep those old
links working by redirecting <code>/posts</code> to <code>/blog</code> inside the viewer request function.</p>
<p>Let&rsquo;s try that. If I visit <code>/post</code>, we&rsquo;re automatically redirected to <code>/blog</code>. If we look in dev
tools, the request to <code>/posts</code> returned a 301 Moved Permanently and the Location header points to
<code>/blog</code>. That redirect is coming directly from the viewer request CloudFront Function we just looked
at.</p>
<p>Now compare that with the Astro development server. If I visit <code>/post</code> there, I just get a 404
because the development server doesn&rsquo;t know about the production redirect rules. With the
simulation, we can test exactly the same redirect behaviour that we&rsquo;ll have in production on AWS
without deploying anything.</p>
<p>So far, we&rsquo;ve simulated the production behaviour locally. But what if there&rsquo;s a bug in one of our
CloudFront Functions and we want to debug it using our IDE. Without Yulin, that&rsquo;s usually quite
difficult and on many projects, it isn&rsquo;t really practical. With Yulin, it&rsquo;s quite straightforward.</p>
<p>Let&rsquo;s go back to our local dev script. At the moment, all it&rsquo;s doing is telling the simulator which
CloudFormation template to deploy. If we look back at the CDK stack, you&rsquo;ll notice that each
CloudFront Function has its source code embedded directly into the generated JSON template. That&rsquo;s
how both real AWS and the simulator normally execute the function.</p>
<p>But in the simulator, we don&rsquo;t have to use the embedded source code. Instead, we can use a feature
called bindings. Bindings let us replace an embedded CloudFront Function with a real JavaScript
function running in the same Node.js process as the simulator.</p>
<p>Let&rsquo;s add one. We&rsquo;ll create a bindings array where each entry replaces one resource in the template.
First, we specify the logical ID of the resource we want to replace. In this case, it&rsquo;s the viewer
request function. Next, we need the handler. We&rsquo;ll import that directly from our source code. Now we
can pass it into the binding.</p>
<p>So what&rsquo;s happening here is we&rsquo;re telling Yulin to find the embedded handler for this CloudFront
Function and replace it at runtime with the real handler function we&rsquo;ve imported from our project.
Everything now runs in a single process, which means our IDE debugger can see it.</p>
<p>Let&rsquo;s stop the simulator and start it again, this time in debug mode. Everything looks much the same
so far. The website still behaves exactly as before.</p>
<p>Now let&rsquo;s go back to the viewer request function and set a breakpoint. I&rsquo;ll put one here. Now when I
refresh the page, the debugger stops on our breakpoint. If we switch back to the browser, it&rsquo;s
waiting for a response because execution is currently paused inside the CloudFront Function.</p>
<p>I&rsquo;ll remove that breakpoint and continue, the page loads normally again. Earlier, we looked at the
legacy redirect, so let&rsquo;s put a breakpoint there instead. If I visit <code>/post</code> we&rsquo;re taken straight
back into the debugger. From here, we can inspect everything that&rsquo;s available to the function. We
can look at the event, the context, the request, and any other values we need while debugging.</p>
<p>Once we&rsquo;re done, we simply continue execution, the redirect is returned, the browser follows it, and
we end up on the correct <code>/blog</code> page, and all of that is enabled by this small local dev script,
only about 28 lines of code.</p>
]]></content:encoded><enclosure url="https://kensiosoftware.co.uk/blog/video-simulate-aws-local-website-host-yulin/video-simulate-aws-local-website-host-yulin.png" type="image/png"/><category>AWS (Amazon Web Services)</category><category>TypeScript &amp; JavaScript</category><category>Node.js</category></item><item><title>@kensio/yulin v0.24.0 adds simulated Route53</title><link>https://kensiosoftware.co.uk/blog/npm-kensio-yulin-simulated-route53/</link><pubDate>Thu, 02 Jul 2026 18:31:15 +0000</pubDate><author>hugh@kensiosoftware.co.uk (Hugh Grigg)</author><guid>https://kensiosoftware.co.uk/blog/npm-kensio-yulin-simulated-route53/</guid><description>@kensio/yulin package v0.24.0 adds simulated AWS Route53, so you can simulate Hosted Zones, Records and CNAME resolution, including sim CloudFormation support</description><content:encoded><![CDATA[<p>Version <code>v0.24.0</code> of the <code>@kensio/yulin</code> npm package adds a simulated Route53 service for local
development and isolated testing.</p>
<p>The simulator supports creating Hosted Zones and record sets through Route53 SDK commands, and also
supports <code>AWS::Route53::HostedZone</code> and <code>AWS::Route53::RecordSet</code> resources when deploying
CloudFormation or CDK templates into the simulated AWS.</p>
<p>One feature that&rsquo;s particularly useful for local development and testing is simulated CNAME
resolution. When Yulin is running on localhost, sim Route53 records can route your own application
hostnames to other simulated AWS services.</p>
<p>For example, you can create a simulated CloudFront distribution, point a Route53 record at it, and
then access it locally using a URL like:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">http://www.example.test.sim-aws.localhost:3000/
</span></span></code></pre></div><p>That makes it possible to exercise applications using realistic hostnames while everything still
runs locally in a single Node.js process.</p>
<p>Creating a Hosted Zone looks much the same as it does against the real AWS SDK:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-typescript" data-lang="typescript"><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">simAws</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">SimAws</span><span class="p">();</span>
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">route53</span> <span class="o">=</span> <span class="nx">simAws</span><span class="p">.</span><span class="nx">route53</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">await</span> <span class="nx">route53</span><span class="p">.</span><span class="nx">createHostedZone</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="k">new</span> <span class="nx">CreateHostedZoneCommand</span><span class="p">({</span>
</span></span><span class="line"><span class="cl">    <span class="nx">Name</span><span class="o">:</span> <span class="s2">&#34;example.test&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nx">CallerReference</span><span class="o">:</span> <span class="s2">&#34;example-zone&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="p">}),</span>
</span></span><span class="line"><span class="cl"><span class="p">);</span>
</span></span></code></pre></div><p>You can also deploy sim Route53 infrastructure directly from CloudFormation or CDK templates. For
example with an output JSON template like:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;Resources&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;SiteZone&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;Type&#34;</span><span class="p">:</span> <span class="s2">&#34;AWS::Route53::HostedZone&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;Properties&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nt">&#34;Name&#34;</span><span class="p">:</span> <span class="s2">&#34;example.test&#34;</span>
</span></span><span class="line"><span class="cl">      <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="p">},</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;SiteRecord&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;Type&#34;</span><span class="p">:</span> <span class="s2">&#34;AWS::Route53::RecordSet&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;Properties&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nt">&#34;HostedZoneId&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">          <span class="nt">&#34;Ref&#34;</span><span class="p">:</span> <span class="s2">&#34;SiteZone&#34;</span>
</span></span><span class="line"><span class="cl">        <span class="p">},</span>
</span></span><span class="line"><span class="cl">        <span class="nt">&#34;Name&#34;</span><span class="p">:</span> <span class="s2">&#34;www.example.test&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="nt">&#34;Type&#34;</span><span class="p">:</span> <span class="s2">&#34;CNAME&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="nt">&#34;TTL&#34;</span><span class="p">:</span> <span class="s2">&#34;300&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="nt">&#34;ResourceRecords&#34;</span><span class="p">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">          <span class="s2">&#34;d111111abcdef8.cloudfront.net&#34;</span>
</span></span><span class="line"><span class="cl">        <span class="p">]</span>
</span></span><span class="line"><span class="cl">      <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>You can deploy that into simulated AWS:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-typescript" data-lang="typescript"><span class="line"><span class="cl"><span class="kr">import</span> <span class="p">{</span> <span class="nx">SimAws</span> <span class="p">}</span> <span class="kr">from</span> <span class="s2">&#34;@kensio/yulin&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">simAws</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">SimAws</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">await</span> <span class="nx">simAws</span><span class="p">.</span><span class="nx">cloudFormation</span><span class="p">().</span><span class="nx">deployTemplateFile</span><span class="p">(</span><span class="s2">&#34;path/to/template.json&#34;</span><span class="p">);</span>
</span></span></code></pre></div><p>You can then interact with the simulated AWS including sim Route53 on localhost and in automated
tests.</p>
<p>The Route53 simulator integrates with the rest of Yulin, so CloudFormation, CDK, S3 and CloudFront
can all participate in the same simulated environment for local development and system
tests.</p>
<p>Documentation: <a href="https://yulinsim.dev/services/route53/" title="Simulated Route53 docs">https://yulinsim.dev/services/route53/</a></p>
<p>GitHub: <a href="https://github.com/KensioSoftware/yulin">https://github.com/KensioSoftware/yulin</a></p>
<p>npm: <a href="https://www.npmjs.com/package/@kensio/yulin">https://www.npmjs.com/package/@kensio/yulin</a></p>
]]></content:encoded><enclosure url="https://kensiosoftware.co.uk/blog/npm-kensio-yulin-simulated-route53/npm-kensio-yulin-simulated-route53-og.png" type="image/png"/><category>AWS (Amazon Web Services)</category><category>TypeScript &amp; JavaScript</category><category>Node.js</category></item><item><title>@kensio/smartass assertions ESLint config rules</title><link>https://kensiosoftware.co.uk/blog/npm-kensio-smartass-eslint-config-specific-assertions/</link><pubDate>Thu, 02 Jul 2026 07:33:02 +0000</pubDate><author>hugh@kensiosoftware.co.uk (Hugh Grigg)</author><guid>https://kensiosoftware.co.uk/blog/npm-kensio-smartass-eslint-config-specific-assertions/</guid><description>As of v1.27.0 the @kensio/smartass npm package now exports ESLint config rules to encourage use of a more specific assertion when one is available. For…</description><content:encoded><![CDATA[<p>As of <code>v1.27.0</code> the <code>@kensio/smartass</code> npm package now exports
<a href="https://github.com/KensioSoftware/smartass/blob/main/src/eslint/smartass-eslint.config.ts" title="Specific assertions ESLint config">ESLint config rules</a>
to encourage use of a more specific assertion when one is available.</p>
<p>For example, for slightly misused assertions like these:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-typescript" data-lang="typescript"><span class="line"><span class="cl"><span class="nx">assertIdentical</span><span class="p">(</span><span class="nx">fooBool</span><span class="p">,</span> <span class="kc">true</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nx">assertIdentical</span><span class="p">(</span><span class="nx">fooList</span><span class="p">.</span><span class="nx">length</span><span class="p">,</span> <span class="mi">2</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nx">assertTrue</span><span class="p">(</span><span class="k">typeof</span> <span class="nx">fooStr</span> <span class="o">===</span> <span class="s2">&#34;string&#34;</span><span class="p">);</span>
</span></span></code></pre></div><p>The ESLint rules will highlight each with a warning and a suggestion to use a more appropriate
assertion instead:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-typescript" data-lang="typescript"><span class="line"><span class="cl"><span class="c1">// ESLint: Use assertTrue(value) instead of assertIdentical(value, true).
</span></span></span><span class="line"><span class="cl"><span class="c1">// (no-restricted-syntax)
</span></span></span><span class="line"><span class="cl"><span class="nx">assertTrue</span><span class="p">(</span><span class="nx">fooBool</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// ESLint: Use a more specific length assertion, such as
</span></span></span><span class="line"><span class="cl"><span class="c1">// assertArrayLength(value, expectedLength) or assertStringLength(value, expectedLength),
</span></span></span><span class="line"><span class="cl"><span class="c1">// instead of assertIdentical(value.length, expectedLength). (no-restricted-syntax)
</span></span></span><span class="line"><span class="cl"><span class="nx">assertArrayLength</span><span class="p">(</span><span class="nx">fooList</span><span class="p">,</span> <span class="mi">2</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// ESLint: Use a more specific type assertion, such as assertTypeString(value),
</span></span></span><span class="line"><span class="cl"><span class="c1">// assertTypeNumber(value), or assertTypeBoolean(value), instead of
</span></span></span><span class="line"><span class="cl"><span class="c1">// assertTrue(typeof value === expectedType). (no-restricted-syntax)
</span></span></span><span class="line"><span class="cl"><span class="nx">assertTypeString</span><span class="p">(</span><span class="nx">fooStr</span><span class="p">);</span>
</span></span></code></pre></div><p>This is useful for reminding human developers about better assertion options, and for improving on
output from LLMs, which tend to overuse the more general assertion functions.</p>
<p>Using the more specific assertion functions allows a project to get more benefit from the type
narrowing effects in the <code>@kensio/smartass</code> library.</p>
<p>Some more examples of the replacements that the ESLint rules will suggest:</p>
<ul>
<li><code>assertIdentical(value, true)</code> → <code>assertTrue(value)</code></li>
<li><code>assertIdentical(value, false)</code> → <code>assertFalse(value)</code></li>
<li><code>assertIdentical(value, undefined)</code> → <code>assertUndefined(value)</code></li>
<li><code>assertTrue(value != null)</code> → <code>assertNonNullable(value)</code></li>
<li><code>assertIdentical(typeof value, &quot;string&quot;)</code> → <code>assertTypeString(value)</code></li>
<li><code>assertIdentical(value instanceof MyClass, true)</code> → <code>assertInstanceOf(value, MyClass)</code></li>
<li><code>assertIdentical(&quot;key&quot; in object, true)</code> → <code>assertObjectHasProperty(object, &quot;key&quot;)</code></li>
<li><code>assertIdentical(value.length, expected)</code> → <code>assertArrayLength()</code> etc.</li>
<li><code>assertIdentical(value.size, expected)</code> → <code>assertSetSize()</code> etc.</li>
<li><code>assertTrue(array.length &gt; 0)</code> → <code>assertArrayNotEmpty(array)</code></li>
<li><code>assertTrue(value.includes(expected))</code> → <code>assertStringIncludes()</code> etc.</li>
<li><code>assertTrue(value.startsWith(prefix))</code>  → <code>assertStringStartsWith(value, prefix)</code></li>
<li><code>assertTrue(value.endsWith(suffix))</code>  → <code>assertStringEndsWith(value, suffix)</code></li>
<li><code>assertTrue(stats.isDirectory())</code> → <code>assertDirectoryExists()</code> etc.</li>
</ul>
<p>You can use the ESLint rules in your project&rsquo;s <code>eslint.config.ts</code> like this:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-typescript" data-lang="typescript"><span class="line"><span class="cl"><span class="kr">import</span> <span class="p">{</span> <span class="nx">defineConfig</span> <span class="p">}</span> <span class="kr">from</span> <span class="s2">&#34;eslint/config&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="kr">import</span> <span class="nx">tseslint</span> <span class="kr">from</span> <span class="s2">&#34;typescript-eslint&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kr">import</span> <span class="p">{</span> <span class="nx">smartassPreferSpecificAssertions</span> <span class="p">}</span> <span class="kr">from</span> <span class="s2">&#34;@kensio/smartass/eslint&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kr">export</span> <span class="k">default</span> <span class="nx">defineConfig</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="p">...</span><span class="nx">tseslint</span><span class="p">.</span><span class="nx">configs</span><span class="p">.</span><span class="nx">recommended</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="p">...</span><span class="nx">smartassPreferSpecificAssertions</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"><span class="p">);</span>
</span></span></code></pre></div><p>And finally, a quick note on how the rules work internally. The rules use ESLint selector syntax to
identify misused assertion functions. For example:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-typescript" data-lang="typescript"><span class="line"><span class="cl"><span class="p">[{</span>
</span></span><span class="line"><span class="cl">  <span class="nx">selector</span><span class="o">:</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;CallExpression[callee.name=&#39;assertIdentical&#39;] &gt; Literal[value=true]:nth-child(2)&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nx">message</span><span class="o">:</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;Use assertTrue(value) instead of assertIdentical(value, true).&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"><span class="p">}]</span>
</span></span></code></pre></div><ul>
<li><code>CallExpression</code> — look for a function call</li>
<li><code>[callee.name='assertIdentical']</code> — where the function being called is assertIdentical</li>
<li><code>&gt; Literal[value=true]</code> — whose argument is the literal <code>true</code></li>
<li><code>:nth-child(2)</code> — specifically the second argument</li>
</ul>
<p>Note how this is similar to CSS selector syntax; it works in a similar way.</p>
<p>npm: <a href="https://www.npmjs.com/package/@kensio/smartass">https://www.npmjs.com/package/@kensio/smartass</a></p>
<p>GitHub: <a href="https://github.com/KensioSoftware/smartass">https://github.com/KensioSoftware/smartass</a></p>
<p>Docs: <a href="https://smartassertions.dev/">https://smartassertions.dev/</a></p>
]]></content:encoded><enclosure url="https://kensiosoftware.co.uk/blog/npm-kensio-smartass-eslint-config-specific-assertions/npm-kensio-smartass-eslint-config-specific-assertions-og.png" type="image/png"/><category>TypeScript &amp; JavaScript</category><category>Node.js</category></item><item><title>@kensio/yulin v0.18.0 adds simulated CloudFormation</title><link>https://kensiosoftware.co.uk/blog/npm-kensio-yulin-simulated-cloudformation/</link><pubDate>Tue, 23 Jun 2026 08:51:09 +0000</pubDate><author>hugh@kensiosoftware.co.uk (Hugh Grigg)</author><guid>https://kensiosoftware.co.uk/blog/npm-kensio-yulin-simulated-cloudformation/</guid><description>@kensio/yulin package v0.18.0 adds simulated CloudFormation, so you can deploy CloudFormation, SAM and CDK JSON templates into simulated AWS for local dev and…</description><content:encoded><![CDATA[<p>Version <code>0.18.0</code> of the
<a href="https://github.com/KensioSoftware/yulin" title="Node.js AWS simulation">@kensio/yulin</a>
package adds
<a href="https://github.com/KensioSoftware/yulin/tree/main/docs/services/cloudformation" title="Node.js CloudFormation simulator">simulated CloudFormation</a>.</p>
<p>This feature allows you to deploy JSON templates from CloudFormation, SAM or CDK into the simulated
AWS. Using your existing CloudFormation templates for local development and testing is convenient
and efficient.</p>
<p>The simulated CloudFormation deployment only takes a few milliseconds and sets up the simulated AWS
resources in the same single process alongside your Node.js application code and tests. You can step
through the whole system in a debugger, as well as collect test coverage metrics from the simulated
system tests.</p>
<p>Given a basic CloudFormation JSON output template like this:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;Resources&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;SiteBucket&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;Type&#34;</span><span class="p">:</span> <span class="s2">&#34;AWS::S3::Bucket&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;Properties&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nt">&#34;BucketName&#34;</span><span class="p">:</span> <span class="s2">&#34;example-site-bucket&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="nt">&#34;WebsiteConfiguration&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">          <span class="nt">&#34;IndexDocument&#34;</span><span class="p">:</span> <span class="s2">&#34;index.html&#34;</span>
</span></span><span class="line"><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="cl">      <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>You can directly deploy it into the simulated AWS via the sim CloudFormation service:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-typescript" data-lang="typescript"><span class="line"><span class="cl"><span class="kr">import</span> <span class="p">{</span> <span class="nx">SimAws</span> <span class="p">}</span> <span class="kr">from</span> <span class="s2">&#34;@kensio/yulin&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">simAws</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">SimAws</span><span class="p">();</span>
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">simCfn</span> <span class="o">=</span> <span class="nx">simAws</span><span class="p">.</span><span class="nx">cloudFormation</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">stack</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">simCfn</span><span class="p">.</span><span class="nx">deployTemplate</span><span class="p">({</span>
</span></span><span class="line"><span class="cl">  <span class="nx">stackName</span><span class="o">:</span> <span class="s2">&#34;site-stack&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nx">template</span><span class="o">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nx">Resources</span><span class="o">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="nx">SiteBucket</span><span class="o">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nx">Type</span><span class="o">:</span> <span class="s2">&#34;AWS::S3::Bucket&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="nx">Properties</span><span class="o">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">          <span class="nx">BucketName</span><span class="o">:</span> <span class="s2">&#34;example-site-bucket&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">          <span class="nx">WebsiteConfiguration</span><span class="o">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="nx">IndexDocument</span><span class="o">:</span> <span class="s2">&#34;index.html&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">          <span class="p">},</span>
</span></span><span class="line"><span class="cl">        <span class="p">},</span>
</span></span><span class="line"><span class="cl">      <span class="p">},</span>
</span></span><span class="line"><span class="cl">    <span class="p">},</span>
</span></span><span class="line"><span class="cl">  <span class="p">},</span>
</span></span><span class="line"><span class="cl"><span class="p">});</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">await</span> <span class="nx">stack</span><span class="p">.</span><span class="nx">waitForDeployComplete</span><span class="p">();</span>
</span></span></code></pre></div><p>That creates the simulated S3 Bucket and configures static website hosting for it according to the
configuration in the JSON template.</p>
<p>There is also a <code>deployTemplateFile()</code> convenience method that takes a file path and deploys the
JSON template at that path into the simulated AWS:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-typescript" data-lang="typescript"><span class="line"><span class="cl"><span class="kr">import</span> <span class="p">{</span> <span class="nx">SimAws</span> <span class="p">}</span> <span class="kr">from</span> <span class="s2">&#34;@kensio/yulin&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">simAws</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">SimAws</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">stack</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">simAws</span>
</span></span><span class="line"><span class="cl">  <span class="p">.</span><span class="nx">cloudFormation</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">  <span class="p">.</span><span class="nx">deployTemplateFile</span><span class="p">(</span><span class="s2">&#34;foo/bar/path/template.json&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">await</span> <span class="nx">stack</span><span class="p">.</span><span class="nx">waitForDeployComplete</span><span class="p">();</span>
</span></span></code></pre></div><p>Because the simulated CloudFormation uses normal JSON output templates, it can deploy templates
created by standard CloudFormation, SAM or CDK. You can use the existing CloudFormation setup for
your project directly with the simulated AWS, making it easy to run realistic system tests and
develop locally on your laptop.</p>
<p>By default the simulated CloudFormation deploys CloudFront Functions from the handler source code
embedded in the JSON template. This works and supports low-effort configuration. There is also an
optional
<a href="https://github.com/KensioSoftware/yulin/tree/main/docs/services/cloudformation#cloudfront-function-bindings">bindings</a>
parameter that you can use to swap in the real handler function implementations
for your CloudFront Functions when deploying into simulated AWS:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-typescript" data-lang="typescript"><span class="line"><span class="cl"><span class="kr">import</span> <span class="p">{</span> <span class="nx">SimAws</span> <span class="p">}</span> <span class="kr">from</span> <span class="s2">&#34;@kensio/yulin&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kr">import</span> <span class="p">{</span> <span class="nx">viewerRequestHandler</span> <span class="p">}</span> <span class="kr">from</span> <span class="s2">&#34;./path/to/viewer-request-handler.js&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">simAws</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">SimAws</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">stack</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">simAws</span>
</span></span><span class="line"><span class="cl">  <span class="p">.</span><span class="nx">cloudFormation</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">  <span class="p">.</span><span class="nx">deployTemplateFile</span><span class="p">({</span>
</span></span><span class="line"><span class="cl">    <span class="nx">templatePath</span><span class="o">:</span> <span class="s2">&#34;cdk.out/TestStack.template.json&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nx">bindings</span><span class="o">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">      <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nx">logicalId</span><span class="o">:</span> <span class="s2">&#34;FooCloudFrontFunction&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="nx">handler</span>: <span class="kt">viewerRequestHandler</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="p">},</span>
</span></span><span class="line"><span class="cl">    <span class="p">],</span>
</span></span><span class="line"><span class="cl">  <span class="p">});</span>
</span></span></code></pre></div><p>The simulated CloudFormation deployment uses the <code>logicalId</code> of the CloudFront Function to swap in
the real handler function in the same process. This allows you to run the whole system simulation
and step through it in a debugger, including inside CloudFront Function handler code. It also allows
you to collect test coverage metrics for your CloudFront Functions.</p>
<p>Docs: <a href="https://yulinsim.dev/services/cloudformation/" title="Simulated CloudFormation docs">https://yulinsim.dev/services/cloudformation/</a></p>
<p>GitHub: <a href="https://github.com/KensioSoftware/yulin" title="Yulin simulated AWS GitHub">https://github.com/KensioSoftware/yulin</a></p>
<p>npm: <a href="https://www.npmjs.com/package/@kensio/yulin" title="Yulin simulated AWS npm package">https://www.npmjs.com/package/@kensio/yulin</a></p>
]]></content:encoded><enclosure url="https://kensiosoftware.co.uk/blog/npm-kensio-yulin-simulated-cloudformation/npm-kensio-yulin-simulated-cloudformation-og.png" type="image/png"/><category>AWS (Amazon Web Services)</category><category>TypeScript &amp; JavaScript</category><category>Node.js</category></item><item><title>@kensio/yulin v0.16.0 adds CloudFront Functions typing support and ESLint config</title><link>https://kensiosoftware.co.uk/blog/npm-kensio-yulin-cloudfront-functions-typing-eslint-config/</link><pubDate>Tue, 16 Jun 2026 07:46:45 +0000</pubDate><author>hugh@kensiosoftware.co.uk (Hugh Grigg)</author><guid>https://kensiosoftware.co.uk/blog/npm-kensio-yulin-cloudfront-functions-typing-eslint-config/</guid><description>@kensio/yulin package v0.16.0 adds TypeScript typing support and ESLint config for AWS CloudFront Functions JS2 syntax</description><content:encoded><![CDATA[<p>Version <code>0.16.0</code> of the
<a href="https://github.com/KensioSoftware/yulin" title="Local AWS simulator npm package">@kensio/yulin</a>
package adds ESLint config and typing support for CloudFront Functions JS2 syntax.</p>
<p>CloudFront Functions types in JavaScript / JS2 files:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="cl"><span class="cm">/**
</span></span></span><span class="line"><span class="cl"><span class="cm">* @typedef {import(&#34;@kensio/yulin/cloudfront&#34;).CloudFrontFunction.Event} CloudFrontEvent
</span></span></span><span class="line"><span class="cl"><span class="cm">* @typedef {import(&#34;@kensio/yulin/cloudfront&#34;).CloudFrontFunction.Request} CloudFrontRequest
</span></span></span><span class="line"><span class="cl"><span class="cm">* @typedef {import(&#34;@kensio/yulin/cloudfront&#34;).CloudFrontFunction.Response} CloudFrontResponse
</span></span></span><span class="line"><span class="cl"><span class="cm">*/</span>
</span></span></code></pre></div><p>CloudFront Functions types in TypeScript files:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-typescript" data-lang="typescript"><span class="line"><span class="cl"><span class="kr">import</span> <span class="p">{</span> <span class="kr">type</span> <span class="nx">CloudFrontFunction</span> <span class="p">}</span> <span class="kr">from</span> <span class="s2">&#34;@kensio/yulin/cloudfront&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">event</span>: <span class="kt">CloudFrontFunction.Event</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="c1">// ...
</span></span></span><span class="line"><span class="cl"><span class="p">};</span>
</span></span></code></pre></div><p>CloudFront Functions JS2 syntax ESLint config in <code>eslint.config.ts</code>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-typescript" data-lang="typescript"><span class="line"><span class="cl"><span class="kr">import</span> <span class="p">{</span> <span class="nx">defineConfig</span> <span class="p">}</span> <span class="kr">from</span> <span class="s2">&#34;eslint/config&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="kr">import</span> <span class="nx">tseslint</span> <span class="kr">from</span> <span class="s2">&#34;typescript-eslint&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kr">import</span> <span class="p">{</span> <span class="nx">cloudFrontFunctionsJs2</span> <span class="p">}</span> <span class="kr">from</span> <span class="s2">&#34;@kensio/yulin/eslint&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kr">export</span> <span class="k">default</span> <span class="nx">defineConfig</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="p">...</span><span class="nx">tseslint</span><span class="p">.</span><span class="nx">configs</span><span class="p">.</span><span class="nx">recommended</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="p">...</span><span class="nx">cloudFrontFunctionsJs2</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"><span class="p">);</span>
</span></span></code></pre></div><p>Then you can work on CFF JS2 in <code>.cff.js</code> files with some basic typing and ESLint support.</p>
<p>Docs: <a href="https://yulinsim.dev/" title="Yulin AWS simulator docs">https://yulinsim.dev/</a></p>
<p>GitHub: <a href="https://github.com/KensioSoftware/yulin" title="Yulin AWS simulator GitHub">https://github.com/KensioSoftware/yulin</a></p>
<p>npm: <a href="https://www.npmjs.com/package/@kensio/yulin" title="Yulin AWS simulator npm package">https://www.npmjs.com/package/@kensio/yulin</a></p>
]]></content:encoded><enclosure url="https://kensiosoftware.co.uk/blog/npm-kensio-yulin-cloudfront-functions-typing-eslint-config/npm-kensio-yulin-cloudfront-functions-typing-eslint-config-og.png" type="image/png"/><category>AWS (Amazon Web Services)</category><category>TypeScript &amp; JavaScript</category><category>Node.js</category></item><item><title>@kensio/smartass 1.15.0 improves overlap-preserving type refinement</title><link>https://kensiosoftware.co.uk/blog/kensio-smartass-overlap-preserving-type-refinement/</link><pubDate>Mon, 15 Jun 2026 18:00:00 +0000</pubDate><author>hugh@kensiosoftware.co.uk (Hugh Grigg)</author><guid>https://kensiosoftware.co.uk/blog/kensio-smartass-overlap-preserving-type-refinement/</guid><description>@kensio/smartass package v1.15.0 improves type refinement so that assertion functions and composable matchers incorporate existing type information from the…</description><content:encoded><![CDATA[<p>Version <code>v1.15.0</code> of the <code>@kensio/smartass</code> package improves type refinement for assertion
functions and composable matchers.</p>
<p>The goal is to incorporate existing type information from the calling scope when possible, rather
than widening values to a less precise asserted type.</p>
<p>For example, consider a value whose type is already known to be:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-typescript" data-lang="typescript"><span class="line"><span class="cl"><span class="kr">import</span> <span class="p">{</span> <span class="nx">assertObjectMatches</span><span class="p">,</span> <span class="nx">typeString</span> <span class="p">}</span> <span class="kr">from</span> <span class="s2">&#34;@kensio/smartass&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kr">interface</span> <span class="nx">Foo</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nx">bar</span><span class="o">?:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nx">foobar</span><span class="o">?:</span> <span class="s2">&#34;hello&#34;</span> <span class="o">|</span> <span class="s2">&#34;world&#34;</span> <span class="o">|</span> <span class="mi">123</span> <span class="o">|</span> <span class="kc">null</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="p">};</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kd">function</span> <span class="nx">getFoo</span><span class="p">()</span><span class="o">:</span> <span class="nx">Foo</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="k">return</span> <span class="p">{</span> <span class="nx">bar</span><span class="o">:</span> <span class="p">{</span> <span class="nx">foobar</span><span class="o">:</span> <span class="s2">&#34;hello&#34;</span> <span class="p">}</span> <span class="p">};</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">foo</span> <span class="o">=</span> <span class="nx">getFoo</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nx">assertObjectMatches</span><span class="p">(</span><span class="nx">foo</span><span class="p">,</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nx">bar</span><span class="o">:</span> <span class="p">{</span> <span class="nx">foobar</span>: <span class="kt">typeString</span><span class="p">()</span> <span class="p">},</span>
</span></span><span class="line"><span class="cl"><span class="p">});</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nx">foo</span><span class="p">.</span><span class="nx">bar</span><span class="p">.</span><span class="nx">foobar</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="c1">// &#34;hello&#34; | &#34;world&#34;
</span></span></span></code></pre></div><p>The <code>typeString()</code> composable matcher works from within <code>assertObjectMatches()</code> to provide type
refinement back to the calling scope. TypeScript can combine this type assertion with the existing
type information in the calling scope to precisely narrow the type of <code>foo.bar.foobar</code> to the
literal union type <code>&quot;hello&quot; | &quot;world&quot;</code>;</p>
<p>Prior to this improvement, it would have been easy for an assertion signature to broaden the type to
just <code>string</code>.</p>
<p>Version <code>v1.15.0</code> updates all the assertion signatures and composable matchers in the package to
provide this overlap-preserving type refinement.</p>
<p>This tends to be helpful in tests, because it combines runtime validation with precise
compile-time type checking. After an assertion succeeds, TypeScript can often infer more specific
types than before, which reduces the need for manual casts and helps IDE autocomplete provide more
accurate suggestions.</p>
<p>npm: <a href="https://www.npmjs.com/package/@kensio/smartass">https://www.npmjs.com/package/@kensio/smartass</a></p>
<p>GitHub: <a href="https://github.com/KensioSoftware/smartass">https://github.com/KensioSoftware/smartass</a></p>
<p>Docs: <a href="https://smartassertions.dev/">https://smartassertions.dev/</a></p>
]]></content:encoded><enclosure url="https://kensiosoftware.co.uk/blog/kensio-smartass-overlap-preserving-type-refinement/kensio-smartass-overlap-preserving-type-refinement-og.png" type="image/png"/><category>TypeScript &amp; JavaScript</category><category>Node.js</category></item><item><title>@kensio/smartass 1.10.0 gets composable type narrowing matchers</title><link>https://kensiosoftware.co.uk/blog/kensio-smartass-type-narrowing-composable-matchers/</link><pubDate>Sun, 14 Jun 2026 10:57:17 +0000</pubDate><author>hugh@kensiosoftware.co.uk (Hugh Grigg)</author><guid>https://kensiosoftware.co.uk/blog/kensio-smartass-type-narrowing-composable-matchers/</guid><description>The @kensio/smartass package now provides composable type narrowing matchers. This allows you to apply structured assertions that provide detailed type…</description><content:encoded><![CDATA[<p>Version <code>v1.10.0</code> of
the <a href="https://github.com/KensioSoftware/smartass/" title="Type narrowing assertion functions">@kensio/smartass</a>
package introduces composable type narrowing matchers.</p>
<p>This allows you to apply structured assertions that provide detailed type narrowing back to
TypeScript:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-typescript" data-lang="typescript"><span class="line"><span class="cl"><span class="kr">import</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nx">assertObjectMatches</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nx">arrayIncluding</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nx">oneOf</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nx">stringOfLength</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span> <span class="kr">from</span> <span class="s2">&#34;@kensio/smartass&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">user</span> <span class="o">=</span> <span class="nx">getUser</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nx">assertObjectMatches</span><span class="p">(</span><span class="nx">user</span><span class="p">,</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nx">role</span>: <span class="kt">oneOf</span><span class="p">([</span><span class="s2">&#34;admin&#34;</span><span class="p">,</span> <span class="s2">&#34;editor&#34;</span><span class="p">,</span> <span class="s2">&#34;viewer&#34;</span><span class="p">]),</span>
</span></span><span class="line"><span class="cl">  <span class="nx">tags</span>: <span class="kt">arrayIncluding</span><span class="p">(</span><span class="s2">&#34;beta&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">  <span class="nx">id</span>: <span class="kt">stringOfLength</span><span class="p">(</span><span class="mi">8</span><span class="p">),</span>
</span></span><span class="line"><span class="cl"><span class="p">});</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// TypeScript knows:
</span></span></span><span class="line"><span class="cl"><span class="c1">//   user.role is &#39;admin&#39; | &#39;editor&#39; | &#39;viewer&#39;
</span></span></span><span class="line"><span class="cl"><span class="c1">//   user.tags is an array with at least one string element
</span></span></span><span class="line"><span class="cl"><span class="c1">//   user.id is a string of length 8 with safe indexing
</span></span></span></code></pre></div><p>As with the
standalone <a href="https://github.com/KensioSoftware/smartass/#assertion-functions">type narrowing assertion functions</a>,
this is made possible by TypeScript&rsquo;s assertion signature feature.</p>
<p>The fluent expectation interfaces in Jest and Vitest are not able to provide that type narrowing
effect back to the compiler, because TypeScript cannot follow an assertion signature through a
fluent interface like:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-typescript" data-lang="typescript"><span class="line"><span class="cl"><span class="kr">import</span> <span class="p">{</span> <span class="nx">expect</span> <span class="p">}</span> <span class="kr">from</span> <span class="s2">&#34;vitest&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nx">expect</span><span class="p">(</span><span class="nx">user</span><span class="p">).</span><span class="nx">toMatchObject</span><span class="p">({</span>
</span></span><span class="line"><span class="cl">  <span class="nx">role</span><span class="o">:</span> <span class="s2">&#34;editor&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"><span class="p">});</span>
</span></span></code></pre></div><p>Due to the <code>expect().toMatchObject()</code> chain, we lose the chance to provide an assertion signature to
TypeScript. It seems like a shame to miss out on the benefit of that type information, so
<code>@kensio/smartass</code> now provides composable type narrowing matchers.</p>
<p>The
<a href="https://github.com/KensioSoftware/smartass/blob/main/src/assert/object-matches/object-matches.assert.ts" title="Deep partial structural assertion function">assertObjectMatches</a>
function applies a deep partial match, so you only need to specify the properties that a particular
test case is interested in.</p>
<p>After calling that assertion function, you can use the narrowed types for the rest of the test. This
tends to reduce the amount of boilerplate code that is required in each test case, which makes the
tests more readable and easier to maintain.</p>
<p>As of version <code>v1.10.0</code>, the following type narrowing matcher functions are available:</p>
<ul>
<li><a href="https://github.com/KensioSoftware/smartass/blob/main/src/assert/array-includes-all/array-includes-all.match.ts">arrayIncludingAll</a></li>
<li><a href="https://github.com/KensioSoftware/smartass/blob/main/src/assert/array-includes/array-includes.match.ts">arrayIncluding</a></li>
<li><a href="https://github.com/KensioSoftware/smartass/blob/main/src/assert/array-length/array-length.match.ts">arrayOfLength</a></li>
<li><a href="https://github.com/KensioSoftware/smartass/blob/main/src/assert/array-min-length/array-min-length.match.ts">arrayOfMinLength</a></li>
<li><a href="https://github.com/KensioSoftware/smartass/blob/main/src/assert/array-not-empty/array-not-empty.match.ts">nonEmptyArray</a></li>
<li><a href="https://github.com/KensioSoftware/smartass/blob/main/src/assert/buffer-equal/buffer-equal.match.ts">bufferEqualTo</a></li>
<li><a href="https://github.com/KensioSoftware/smartass/blob/main/src/assert/instance-of/instance-of.match.ts">instanceOf</a></li>
<li><a href="https://github.com/KensioSoftware/smartass/blob/main/src/assert/non-nullable/non-nullable.match.ts">nonNullable</a></li>
<li><a href="https://github.com/KensioSoftware/smartass/blob/main/src/assert/number-between/number-between.match.ts">numberBetween</a></li>
<li><a href="https://github.com/KensioSoftware/smartass/blob/main/src/assert/number-to-nearest/number-to-nearest.match.ts">numberToNearest</a></li>
<li><a href="https://github.com/KensioSoftware/smartass/blob/main/src/assert/object-has-property/object-has-property.match.ts">objectWithProperty</a></li>
<li><a href="https://github.com/KensioSoftware/smartass/blob/main/src/assert/one-of/one-of.match.ts">oneOf</a></li>
<li><a href="https://github.com/KensioSoftware/smartass/blob/main/src/assert/string-ends-with/string-ends-with.match.ts">stringEndingWith</a></li>
<li><a href="https://github.com/KensioSoftware/smartass/blob/main/src/assert/string-includes/string-includes.match.ts">stringIncluding</a></li>
<li><a href="https://github.com/KensioSoftware/smartass/blob/main/src/assert/string-length/string-length.match.ts">stringOfLength</a></li>
<li><a href="https://github.com/KensioSoftware/smartass/blob/main/src/assert/string-not-includes/string-not-includes.match.ts">stringNotIncluding</a></li>
<li><a href="https://github.com/KensioSoftware/smartass/blob/main/src/assert/string-starts-with/string-starts-with.match.ts">stringStartingWith</a></li>
<li><a href="https://github.com/KensioSoftware/smartass/blob/main/src/assert/type-bigint/type-bigint.match.ts">typeBigInt</a></li>
<li><a href="https://github.com/KensioSoftware/smartass/blob/main/src/assert/type-boolean/type-boolean.match.ts">typeBoolean</a></li>
<li><a href="https://github.com/KensioSoftware/smartass/blob/main/src/assert/type-function/type-function.match.ts">typeFunction</a></li>
<li><a href="https://github.com/KensioSoftware/smartass/blob/main/src/assert/type-number/type-number.match.ts">typeNumber</a></li>
<li><a href="https://github.com/KensioSoftware/smartass/blob/main/src/assert/type-numeric/type-numeric.match.ts">typeNumeric</a></li>
<li><a href="https://github.com/KensioSoftware/smartass/blob/main/src/assert/type-object/type-object.match.ts">typeObject</a></li>
<li><a href="https://github.com/KensioSoftware/smartass/blob/main/src/assert/type-string/type-string.match.ts">typeString</a></li>
<li><a href="https://github.com/KensioSoftware/smartass/blob/main/src/assert/type-symbol/type-symbol.match.ts">typeSymbol</a></li>
<li><a href="https://github.com/KensioSoftware/smartass/blob/main/src/assert/type-typed-array/type-typed-array.match.ts">typeTypedArray</a></li>
<li><a href="https://github.com/KensioSoftware/smartass/blob/main/src/assert/uuid/uuid-v4.match.ts">uuidV4</a></li>
</ul>
<p>These can be composed together in partial structures for precise type narrowing assertions in tests.</p>
<p>npm: <a href="https://www.npmjs.com/package/@kensio/smartass">https://www.npmjs.com/package/@kensio/smartass</a></p>
<p>GitHub: <a href="https://github.com/KensioSoftware/smartass">https://github.com/KensioSoftware/smartass</a></p>
<p>Docs: <a href="https://smartassertions.dev/">https://smartassertions.dev/</a></p>
]]></content:encoded><enclosure url="https://kensiosoftware.co.uk/blog/kensio-smartass-type-narrowing-composable-matchers/kensio-smartass-type-narrowing-composable-matchers-og.png" type="image/png"/><category>TypeScript &amp; JavaScript</category><category>Node.js</category></item><item><title>@kensio/yulin TypeScript AWS simulator npm package</title><link>https://kensiosoftware.co.uk/blog/npm-kensio-yulin-typescript-aws-simulator-package/</link><pubDate>Sun, 07 Jun 2026 07:42:13 +0000</pubDate><author>hugh@kensiosoftware.co.uk (Hugh Grigg)</author><guid>https://kensiosoftware.co.uk/blog/npm-kensio-yulin-typescript-aws-simulator-package/</guid><description>The @kensio/yulin package is an AWS simulator for Node.js TypeScript applications. It’s conceptually related to moto for Python and LocalStack, but is a bit…</description><content:encoded><![CDATA[<p>The <a href="https://github.com/KensioSoftware/yulin" title="TypeScript AWS simulator">@kensio/yulin</a> package is
an AWS simulator for Node.js TypeScript applications. It&rsquo;s conceptually related
to <a href="https://github.com/getmoto/moto">moto</a> for Python
and <a href="https://www.localstack.cloud/">LocalStack</a>, but is a bit different to both of those.</p>
<p>Yulin simulates AWS services entirely in-process, allowing tests and local development environments
to interact with realistic AWS behaviour without requiring real AWS infrastructure, Docker
containers or any i/o at all.</p>
<p>This differs from traditional test mocking by simulating internal state and overall behaviour.
Traditional mocks don&rsquo;t tend to provide a lot of value, and often end up being detrimental by
requiring so much repetitive boilerplate.</p>
<p>The same Yulin configuration can be shared between tests and local development servers, so you can
define your AWS simulation once and then use it for both those purposes. Because it all runs in the
same single-threaded Node.js process, you can step through the whole system (AWS plus your
applications) in the debugger. This also means you can bring in other testing tools
like <a href="https://github.com/nock/nock">nock HTTP interception</a>
and <a href="https://vitest.dev/guide/mocking/dates">vitest fake timers</a>.</p>
<p>Yulin is still in an early stage and currently only simulates a small subset of AWS services and
behaviours. The long-term goal is to allow simulating multiple complex applications that involve
several AWS services together, such as S3, CloudFront, SQS, DynamoDB and Lambda.</p>
<h2 id="simulate-dont-mock">Simulate, don&rsquo;t mock</h2>
<p>There are already several ways to test AWS-based applications.</p>
<p>At one end of the spectrum are traditional pure unit tests with heavy mocking of dependencies. These
are usually fast, but the mocking tends to be fragile and difficult to maintain. A Lambda that
writes to DynamoDB might be tested by mocking the DynamoDB client and asserting that a particular
method was called with particular arguments. Any time there&rsquo;s a change related to that, the tests
and mocks have to be meticulously updated.</p>
<p>That kind of test can be useful, but they only verify a narrow slice of the overall system
behaviour. They also tend to become brittle, as implementation details leak into the tests, and the
mocks cannot keep up with ongoing changes to the system design.</p>
<p>At the other end of the spectrum are integration tests running against real AWS infrastructure. In
theory, these can provide much stronger confidence, but they are slower, more expensive and require
even more setup and maintenance. They are also at least as fragile as pure unit tests with mocks,
but in a different way. Integration tests are prone to false positives due to networking issues,
cold-starts and asynchronous behaviour.</p>
<p>Tools like LocalStack do bridge the gap between those two extremes, but in practice they often
require even more setup and configuration than integration tests that hit real AWS infrastructure.</p>
<p>Many systems would benefit from tests that exercise realistic interactions between AWS services
while still remaining fast, isolated and easy to run locally.</p>
<h2 id="simulating-aws-behaviour">Simulating AWS behaviour</h2>
<p>Yulin takes the approach of simulating AWS services directly inside the Node.js process, while
avoiding magic and clever tricks.</p>
<p>A test can create a simulated AWS environment, configure services and then execute application code
against that environment. Everything runs in memory and within the same process.</p>
<p>Because there is no real networking involved, tests remain fast. There are no containers to start,
no infrastructure to provision and no external dependencies to coordinate.</p>
<p>This also makes local development easier. The same simulated environment can be used both by
automated tests and for running the system locally and interacting with it via localhost.</p>
<h2 id="testing-systems-instead-of-method-calls">Testing systems instead of method calls</h2>
<p>One benefit of this approach is that it shifts the focus of testing away from implementation details
and towards meaningful system behaviour.</p>
<p>Rather than asserting that a particular SDK method was called, a test can exercise a larger workflow
and verify the resulting system state.</p>
<p>For example, a test might verify that a Lambda processes an event correctly, updates data in
DynamoDB and publishes the expected messages to SQS. The exact implementation details are often less
important than whether the overall behaviour is correct.</p>
<p>This kind of testing tends to align more closely with the behaviours that users and stakeholders
actually care about.</p>
<h2 id="local-development">Local development</h2>
<p>As a software engineering contractor, I see a lot of teams struggle or give up with getting their
system to run locally on engineers&rsquo; laptops. That then leads to contention on development
environments in AWS, which are also expensive to operate and maintain.</p>
<p>More importantly, there is a large hidden cost when engineers cannot iterate rapidly on the
applications they are building. If deploying to AWS is the only option for properly testing changes,
the development iteration cycle gets bogged down, and before long one cycle can take hours or even
days. If it&rsquo;s possible to run the system locally on your laptop, that can instead be minutes or
seconds.</p>
<p>To solve that problem, Yulin can expose on localhost the simulated AWS services and the applications
running amongst them. This allows other local applications, development tools and browsers to
interact with applications in the simulated AWS environment using realistic endpoints and
behaviours.</p>
<p>One small example is static website hosting via S3. Yulin can serve a simulated S3 website locally
while still supporting S3 website features such as routing rules, redirects and error documents.
That is integrated with the rest of the simulation, so you can have a DynamoDB stream trigger a
Lambda function that writes an object to an S3 website bucket. You can then view the resulting web
page in your browser on localhost.</p>
<p>Using the same configuration for local development and automated tests can help reduce duplication
and make development environments more representative of production behaviour.</p>
<h2 id="early-stages">Early stages</h2>
<p>I have to emphasise that Yulin is still a work in progress. Only a small subset of AWS services and
behaviours are implemented so far, and Yulin&rsquo;s APIs will evolve as the project progresses. The focus
at this stage is implementing the behaviours that I happen to need for my own projects.</p>
<p>Even in its current minimal state, Yulin is already useful to me for quickly iterating and testing
on projects.</p>
<p>Docs:</p>
<p><a href="https://yulinsim.dev/">https://yulinsim.dev/</a></p>
<p>GitHub:</p>
<p><a href="https://github.com/KensioSoftware/yulin">https://github.com/KensioSoftware/yulin</a></p>
<p>npm:</p>
<p><a href="https://www.npmjs.com/package/@kensio/yulin">https://www.npmjs.com/package/@kensio/yulin</a></p>
]]></content:encoded><enclosure url="https://kensiosoftware.co.uk/blog/npm-kensio-yulin-typescript-aws-simulator-package/npm-kensio-yulin-typescript-aws-simulator-package-og.png" type="image/png"/><category>AWS (Amazon Web Services)</category><category>TypeScript &amp; JavaScript</category><category>Node.js</category></item><item><title>@kensio/smartass type narrowing assertion function npm package</title><link>https://kensiosoftware.co.uk/blog/npm-kensio-smartass-type-narrowing-assertion-function-package/</link><pubDate>Sun, 07 Jun 2026 07:42:13 +0000</pubDate><author>hugh@kensiosoftware.co.uk (Hugh Grigg)</author><guid>https://kensiosoftware.co.uk/blog/npm-kensio-smartass-type-narrowing-assertion-function-package/</guid><description>The @kensio/smartass package provides TypeScript-first test assertion functions with precise type narrowing. The assertion functions provide type information…</description><content:encoded><![CDATA[<p>The <a href="https://github.com/KensioSoftware/smartass" title="TypeScript type narrowing assertion functions">@kensio/smartass</a>
package provides TypeScript-first test assertion functions with precise type narrowing. The
assertion functions provide type information to TypeScript, which makes it easier to write
straightforward readable tests with assistance from the type system.</p>
<p>Most JavaScript and TypeScript test frameworks provide assertions through APIs such as:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-typescript" data-lang="typescript"><span class="line"><span class="cl"><span class="nx">expect</span><span class="p">(</span><span class="nx">user</span><span class="p">).</span><span class="nx">toBeDefined</span><span class="p">();</span>
</span></span><span class="line"><span class="cl"><span class="nx">expect</span><span class="p">(</span><span class="nx">status</span><span class="p">).</span><span class="nx">toBe</span><span class="p">(</span><span class="s2">&#34;active&#34;</span><span class="p">);</span>
</span></span></code></pre></div><p>These are readable and widely used, but they generally cannot communicate additional type
information back to TypeScript. The assertion checks something at runtime, but the compiler is
unable to follow the types through the fluent interface. This means that calling the assertion does
not update the type information in the test.</p>
<p>Updating type information is what type narrowing means. The <code>@kensio/smartass</code> package does that via
a TypeScript feature called <a href="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-7.html#assertion-functions">assertion signatures</a>.</p>
<p>This means that each assertion in a test provides more type information to TypeScript. This lets you
avoid boilerplate type casting, as well as convert those type casts into runtime assertions so the
test validation is stronger and clearer.</p>
<h2 id="the-basic-idea">The basic idea</h2>
<p>Consider a value that might be undefined:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-typescript" data-lang="typescript"><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">user</span>: <span class="kt">User</span> <span class="o">|</span> <span class="kc">undefined</span> <span class="o">=</span> <span class="nx">getUser</span><span class="p">();</span>
</span></span></code></pre></div><p>A normal runtime check narrows the type:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-typescript" data-lang="typescript"><span class="line"><span class="cl"><span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">user</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="k">throw</span> <span class="k">new</span> <span class="nb">Error</span><span class="p">(</span><span class="s2">&#34;User not found&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nx">user</span><span class="p">.</span><span class="nx">name</span><span class="p">;</span>
</span></span></code></pre></div><p>TypeScript understands that <code>user</code> cannot be undefined after the check.</p>
<p>An assertion function allows us to package that inference of type information into a reusable
function:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-typescript" data-lang="typescript"><span class="line"><span class="cl"><span class="kr">import</span> <span class="p">{</span> <span class="nx">assertNonNullable</span> <span class="p">}</span> <span class="kr">from</span> <span class="s2">&#34;@kensio/smartass&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">user</span>: <span class="kt">User</span> <span class="o">|</span> <span class="kc">undefined</span> <span class="o">=</span> <span class="nx">getUser</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nx">assertNonNullable</span><span class="p">(</span><span class="nx">user</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nx">user</span><span class="p">.</span><span class="nx">name</span><span class="p">;</span>
</span></span></code></pre></div><p>The assertion throws if the value is <code>null</code> or <code>undefined</code>, but it also tells TypeScript that <code>user</code>
cannot be undefined. TypeScript uses that information to infer that <code>user</code> must therefore be an
instance of the <code>User</code> type.</p>
<p>As a result, there is no need for optional chaining or non-null assertions afterwards. E.g. with
vitest assertions:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-typescript" data-lang="typescript"><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">user</span>: <span class="kt">User</span> <span class="o">|</span> <span class="kc">undefined</span> <span class="o">=</span> <span class="nx">getUser</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nx">expect</span><span class="p">(</span><span class="nx">user</span><span class="o">?</span><span class="p">.</span><span class="nx">name</span><span class="p">).</span><span class="nx">toBe</span><span class="p">(</span><span class="s2">&#34;foobar&#34;</span><span class="p">);</span>
</span></span></code></pre></div><p>Due to the lack of type signatures in that fluent assertion chain, we have to use the null-chain
operator <code>?</code> for the rest of the test. In this example it&rsquo;s trivial, but in more complex tests with
larger object structures, it can lead to a lot of clutter in the test.</p>
<p>Moving the type inference into an explicit assertion function is also beneficial by leading to
more detailed validation in the test:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-typescript" data-lang="typescript"><span class="line"><span class="cl"><span class="kr">import</span> <span class="p">{</span> <span class="nx">assertNonNullable</span><span class="p">,</span> <span class="nx">assertIdentical</span> <span class="p">}</span> <span class="kr">from</span> <span class="s2">&#34;@kensio/smartass&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">user</span>: <span class="kt">User</span> <span class="o">|</span> <span class="kc">undefined</span> <span class="o">=</span> <span class="nx">getUser</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nx">assertNonNullable</span><span class="p">(</span><span class="nx">user</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="nx">assertIdentical</span><span class="p">(</span><span class="nx">user</span><span class="p">.</span><span class="nx">name</span><span class="p">,</span> <span class="s2">&#34;foobar&#34;</span><span class="p">);</span>
</span></span></code></pre></div><p>With this approach, if <code>user</code> is undefined, we get a clear assertion failure message. IDEs like
WebStorm can also highlight the specific assertion that failed, and link to it from test output,
which also speeds up debugging.</p>
<h2 id="why-assertion-signatures-are-useful">Why assertion signatures are useful</h2>
<p>Many assertion libraries focus on runtime behaviour at the expense of the type system. They verify
conditions and produce useful failure messages, but the compiler cannot usually use those assertions
when analysing code. It seems like a shame to miss out on such a large potential benefit in
TypeScript.</p>
<p>For example:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-typescript" data-lang="typescript"><span class="line"><span class="cl"><span class="nx">expect</span><span class="p">(</span><span class="nx">user</span><span class="p">).</span><span class="nx">toBeDefined</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nx">user</span><span class="p">.</span><span class="nx">name</span><span class="p">;</span>
</span></span></code></pre></div><p>TypeScript still considers <code>user</code> to be <code>User | undefined</code>.</p>
<p>By contrast:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-typescript" data-lang="typescript"><span class="line"><span class="cl"><span class="nx">assertNonNullable</span><span class="p">(</span><span class="nx">user</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nx">user</span><span class="p">.</span><span class="nx">name</span><span class="p">;</span>
</span></span></code></pre></div><p>TypeScript knows <code>user</code> must be exactly <code>User</code>.</p>
<p>The assertion signature narrows the type, so IntelliSense, autocomplete and compile-time checking
all benefit from the assertion.</p>
<p>This seems trivial with this tiny example, but other more advanced assertion functions in
<code>@kensio/smartass</code> can provide precise type narrowing.</p>
<p>This becomes particularly useful when dealing with values coming from APIs, databases, configuration
files or external systems where the runtime shape is often broader than the shape expected by the
rest of the application.</p>
<h2 id="narrowing-to-specific-values">Narrowing to specific values</h2>
<p>Assertion signatures are not limited to null checking.</p>
<p>For example, <code>assertOneOf()</code> can narrow a value to a specific set of allowed values:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-typescript" data-lang="typescript"><span class="line"><span class="cl"><span class="kr">import</span> <span class="p">{</span> <span class="nx">assertOneOf</span> <span class="p">}</span> <span class="kr">from</span> <span class="s2">&#34;@kensio/smartass&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">status</span> <span class="o">=</span> <span class="nx">getStatus</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nx">assertOneOf</span><span class="p">(</span><span class="nx">status</span><span class="p">,</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">  <span class="s2">&#34;pending&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="s2">&#34;active&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="s2">&#34;completed&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"><span class="p">]);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// status is now:
</span></span></span><span class="line"><span class="cl"><span class="c1">// &#34;pending&#34; | &#34;active&#34; | &#34;completed&#34;
</span></span></span></code></pre></div><p>The <code>assertOneOf()</code> function doesn&rsquo;t just tell TypeScript that the input is of type string. It
narrows the type down as much as possible to the literal union type.</p>
<h2 id="array-assertions">Array assertions</h2>
<p>The package also includes assertions that provide stronger guarantees about arrays.</p>
<p>For example:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-typescript" data-lang="typescript"><span class="line"><span class="cl"><span class="kr">import</span> <span class="p">{</span> <span class="nx">assertArrayNotEmpty</span> <span class="p">}</span> <span class="kr">from</span> <span class="s2">&#34;@kensio/smartass&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">users</span>: <span class="kt">User</span><span class="p">[]</span> <span class="o">=</span> <span class="nx">getUsers</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nx">assertArrayNotEmpty</span><span class="p">(</span><span class="nx">users</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">firstUser</span> <span class="o">=</span> <span class="nx">users</span><span class="p">[</span><span class="mi">0</span><span class="p">];</span>
</span></span></code></pre></div><p>After the assertion, TypeScript understands that the array contains at least one item, so there&rsquo;s no
need for the null-chaining operator <code>?</code> on <code>users[0]</code>.</p>
<p>You can also assert on specific numbers of items in an array with similar precise type narrowing:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-typescript" data-lang="typescript"><span class="line"><span class="cl"><span class="kr">import</span> <span class="p">{</span> <span class="nx">assertArrayLength</span> <span class="p">}</span> <span class="kr">from</span> <span class="s2">&#34;@kensio/smartass&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">users</span>: <span class="kt">User</span><span class="p">[]</span> <span class="o">=</span> <span class="nx">getUsers</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nx">assertArrayLength</span><span class="p">(</span><span class="nx">users</span><span class="p">,</span> <span class="mi">3</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">thirdUser</span> <span class="o">=</span> <span class="nx">users</span><span class="p">[</span><span class="mi">2</span><span class="p">];</span>
</span></span></code></pre></div><p>Again, the <code>assertArrayLength()</code> function is able to tell TypeScript that <code>users[2]</code> exists.
TypeScript can then infer from there that <code>users[2]</code> must be of type <code>User</code>.</p>
<h2 id="runtime-validation-and-type-information-together">Runtime validation and type information together</h2>
<p>One of the recurring themes in TypeScript codebases is that runtime validation and static typing are
often treated as separate concerns. Runtime validation is used in tests to confirm expected
behaviour, while the type system might be used to provide some haphazard assistance and linting.</p>
<p>Assertion functions with type narrowing assertion signatures bring those two concerns together in
tests.</p>
<p>Instead of validating a value and then manually convincing TypeScript that the value is safe, the
assertion function lets you get both benefits together in one line.</p>
<p>The primary goal is not to create another testing DSL but to provide small assertion functions that
improve both runtime safety and type inference. The assertion functions can be used in any test
framework such as Jest or Vitest.</p>
<p>At the time of writing, `` provides the following assertion functions, each of which has the most
precise type narrowing possible:</p>
<ul>
<li><a href="https://github.com/KensioSoftware/smartass/blob/main/src/assert/array-equals/array-equals.assert.ts">assertArrayEquals</a></li>
<li><a href="https://github.com/KensioSoftware/smartass/blob/main/src/assert/array-includes/array-includes.assert.ts">assertArrayIncludes</a></li>
<li><a href="https://github.com/KensioSoftware/smartass/blob/main/src/assert/array-includes-all/array-includes-all.assert.ts">assertArrayIncludesAll</a></li>
<li><a href="https://github.com/KensioSoftware/smartass/blob/main/src/assert/array-length/array-length.assert.ts">assertArrayLength</a></li>
<li><a href="https://github.com/KensioSoftware/smartass/blob/main/src/assert/array-min-length/array-min-length.assert.ts">assertArrayMinLength</a></li>
<li><a href="https://github.com/KensioSoftware/smartass/blob/main/src/assert/array-not-empty/array-not-empty.assert.ts">assertArrayNotEmpty</a></li>
<li><a href="https://github.com/KensioSoftware/smartass/blob/main/src/assert/buffer-equal/buffer-equal.assert.ts">assertBufferEqual</a></li>
<li><a href="https://github.com/KensioSoftware/smartass/blob/main/src/assert/false/false.assert.ts">assertFalse</a></li>
<li><a href="https://github.com/KensioSoftware/smartass/blob/main/src/assert/identical/identical.assert.ts">assertIdentical</a></li>
<li><a href="https://github.com/KensioSoftware/smartass/blob/main/src/assert/instance-of/instance-of.assert.ts">assertInstanceOf</a></li>
<li><a href="https://github.com/KensioSoftware/smartass/blob/main/src/assert/non-nullable/non-nullable.assert.ts">assertNonNullable</a></li>
<li><a href="https://github.com/KensioSoftware/smartass/blob/main/src/assert/number-between/number-between.assert.ts">assertNumberBetween</a></li>
<li><a href="https://github.com/KensioSoftware/smartass/blob/main/src/assert/number-to-nearest/number-to-nearest.assert.ts">assertNumberToNearest</a></li>
<li><a href="https://github.com/KensioSoftware/smartass/blob/main/src/assert/object-equals/object-equals.assert.ts">assertObjectEquals</a></li>
<li><a href="https://github.com/KensioSoftware/smartass/blob/main/src/assert/object-matches/object-matches.assert.ts">assertObjectMatches</a></li>
<li><a href="https://github.com/KensioSoftware/smartass/blob/main/src/assert/one-of/one-of.assert.ts">assertOneOf</a></li>
<li><a href="https://github.com/KensioSoftware/smartass/blob/main/src/assert/string-ends-with/string-ends-with.assert.ts">assertStringEndsWith</a></li>
<li><a href="https://github.com/KensioSoftware/smartass/blob/main/src/assert/string-includes/string-includes.assert.ts">assertStringIncludes</a></li>
<li><a href="https://github.com/KensioSoftware/smartass/blob/main/src/assert/string-not-includes/string-not-includes.assert.ts">assertStringNotIncludes</a></li>
<li><a href="https://github.com/KensioSoftware/smartass/blob/main/src/assert/string-starts-with/string-starts-with.assert.ts">assertStringStartsWith</a></li>
<li><a href="https://github.com/KensioSoftware/smartass/blob/main/src/assert/throws-error/throws-error.assert.ts">assertThrowsError</a></li>
<li><a href="https://github.com/KensioSoftware/smartass/blob/main/src/assert/throws-error-async/throws-error-async.assert.ts">assertThrowsErrorAsync</a></li>
<li><a href="https://github.com/KensioSoftware/smartass/blob/main/src/assert/true/true.assert.ts">assertTrue</a></li>
<li><a href="https://github.com/KensioSoftware/smartass/blob/main/src/assert/type-bigint/type-bigint.assert.ts">assertTypeBigInt</a></li>
<li><a href="https://github.com/KensioSoftware/smartass/blob/main/src/assert/type-boolean/type-boolean.assert.ts">assertTypeBoolean</a></li>
<li><a href="https://github.com/KensioSoftware/smartass/blob/main/src/assert/type-function/type-function.assert.ts">assertTypeFunction</a></li>
<li><a href="https://github.com/KensioSoftware/smartass/blob/main/src/assert/type-number/type-number.assert.ts">assertTypeNumber</a></li>
<li><a href="https://github.com/KensioSoftware/smartass/blob/main/src/assert/type-numeric/type-numeric.assert.ts">assertTypeNumeric</a></li>
<li><a href="https://github.com/KensioSoftware/smartass/blob/main/src/assert/type-object/type-object.assert.ts">assertTypeObject</a></li>
<li><a href="https://github.com/KensioSoftware/smartass/blob/main/src/assert/type-string/type-string.assert.ts">assertTypeString</a></li>
<li><a href="https://github.com/KensioSoftware/smartass/blob/main/src/assert/type-typed-array/type-typed-array.assert.ts">assertTypeTypedArray</a></li>
<li><a href="https://github.com/KensioSoftware/smartass/blob/main/src/assert/undefined/undefined.assert.ts">assertUndefined</a></li>
<li><a href="https://github.com/KensioSoftware/smartass/blob/main/src/assert/uuid/uuid-v4.assert.ts">assertUuidV4</a></li>
</ul>
<h2 id="installing-it">Installing it</h2>
<p>npm: <a href="https://www.npmjs.com/package/@kensio/smartass">https://www.npmjs.com/package/@kensio/smartass</a></p>
<p>GitHub: <a href="https://github.com/KensioSoftware/smartass">https://github.com/KensioSoftware/smartass</a></p>
<p>Docs: <a href="https://smartassertions.dev/">https://smartassertions.dev/</a></p>
]]></content:encoded><enclosure url="https://kensiosoftware.co.uk/blog/npm-kensio-smartass-type-narrowing-assertion-function-package/npm-kensio-smartass-type-narrowing-assertion-function-package-og.png" type="image/png"/><category>TypeScript &amp; JavaScript</category><category>Node.js</category></item><item><title>@kensio/part-factory test object factory npm package</title><link>https://kensiosoftware.co.uk/blog/npm-kensio-part-factory-test-object-factory-package/</link><pubDate>Sun, 07 Jun 2026 06:42:13 +0000</pubDate><author>hugh@kensiosoftware.co.uk (Hugh Grigg)</author><guid>https://kensiosoftware.co.uk/blog/npm-kensio-part-factory-test-object-factory-package/</guid><description>The @kensio/part-factory package provides a minimalist object factory pattern with strong TypeScript typing. When writing tests, it’s common to need objects…</description><content:encoded><![CDATA[<p>The
<a href="https://github.com/KensioSoftware/part-factory" title="TypeScript test object factory pattern package">@kensio/part-factory</a>
package provides a minimalist object factory pattern with strong TypeScript typing.</p>
<p>When writing tests, it&rsquo;s common to need objects that are mostly boilerplate. A test might only care
that a user has a particular email address, but creating that user object may require a username,
profile settings, timestamps and a collection of other properties. As those objects grow, the setup
code can easily become more prominent than the behaviour being tested.</p>
<p><em>Part Factory</em> can help to make that test setup more concise and readable. A factory defines a
complete default object, and each test overrides only the properties that are relevant to the
scenario being tested.</p>
<h2 id="the-basic-idea">The basic idea</h2>
<p>A factory contains the default values for an object:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-typescript" data-lang="typescript"><span class="line"><span class="cl"><span class="kr">interface</span> <span class="nx">Foo</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nx">name</span>: <span class="kt">string</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="nx">size</span>: <span class="kt">number</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="kr">import</span> <span class="p">{</span> <span class="nx">StaticFactory</span> <span class="p">}</span> <span class="kr">from</span> <span class="s2">&#34;@kensio/part-factory&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">fooFactory</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">StaticFactory</span><span class="p">&lt;</span><span class="nt">Foo</span><span class="p">&gt;({</span>
</span></span><span class="line"><span class="cl">  <span class="nx">name</span><span class="o">:</span> <span class="s2">&#34;Foobar&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nx">size</span>: <span class="kt">10</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"><span class="p">});</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">defaultFoo</span> <span class="o">=</span> <span class="nx">fooFactory</span><span class="p">.</span><span class="nx">make</span><span class="p">();</span>
</span></span><span class="line"><span class="cl"><span class="c1">// { name: &#34;Foobar&#34;, size: 10 }
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">myFoo</span> <span class="o">=</span> <span class="nx">fooFactory</span><span class="p">.</span><span class="nx">make</span><span class="p">({</span> <span class="nx">size</span>: <span class="kt">20</span> <span class="p">});</span>
</span></span><span class="line"><span class="cl"><span class="c1">// { name: &#34;Foobar&#34;, size: 20 }
</span></span></span></code></pre></div><p>Instead of repeating every property in every test, the test only specifies what is different. The
defaults remain in one place, making the test setup easier to read and reducing the amount of noise
surrounding the important assertions.</p>
<p>There is a recursive <code>DeepPartial&lt;T&gt;</code> type in <code>@kensio/part-factory</code>, so TypeScript can follow the
partially overridden properties in each test case.</p>
<h2 id="why-use-a-factory-pattern">Why use a factory pattern?</h2>
<p>For small objects with a couple of properties, plain object literals are unproblematic. The benefit
of a factory pattern like this becomes more obvious when the same type appears throughout a test
suite, when objects are deeply nested, or when application types evolve over time.</p>
<p>A common maintenance problem is that object construction becomes scattered across dozens or hundreds
of tests. If a type gains a new required property, many tests may need updating even though they
have no interest in that property. By centralising defaults inside a factory, those changes can
be handled in one location.</p>
<p>Tests that care about the new property can still override it explicitly, while the rest continue to
use the default value provided by the factory.</p>
<h2 id="static-and-dynamic-defaults">Static and dynamic defaults</h2>
<p><em>Part Factory</em> provides two approaches to default values.</p>
<p><code>StaticFactory</code> uses fixed values, which is enough for simple test data where we don&rsquo;t need to test
differing values. AWS SDK event and request structures often work well with static factories.</p>
<p>The alternative <code>DynamicFactory</code> generates fresh values each time a test object is created, making
it useful for IDs, timestamps, names and other generated data where we want to test differing
values.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-typescript" data-lang="typescript"><span class="line"><span class="cl"><span class="kr">import</span> <span class="p">{</span> <span class="nx">DynamicFactory</span> <span class="p">}</span> <span class="kr">from</span> <span class="s2">&#34;@kensio/part-factory&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="kr">import</span> <span class="p">{</span> <span class="nx">faker</span> <span class="p">}</span> <span class="kr">from</span> <span class="s2">&#34;@faker-js/faker&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">fooFactory</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">DynamicFactory</span><span class="p">&lt;</span><span class="nt">Foo</span><span class="p">&gt;(()</span> <span class="o">=&gt;</span> <span class="p">({</span>
</span></span><span class="line"><span class="cl">  <span class="nx">name</span>: <span class="kt">faker.word.noun</span><span class="p">(),</span>
</span></span><span class="line"><span class="cl">  <span class="nx">size</span>: <span class="kt">faker.number.int</span><span class="p">({</span> <span class="nx">max</span>: <span class="kt">100</span> <span class="p">}),</span>
</span></span><span class="line"><span class="cl"><span class="p">}));</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">defaultFoo</span> <span class="o">=</span> <span class="nx">fooFactory</span><span class="p">.</span><span class="nx">make</span><span class="p">();</span>
</span></span><span class="line"><span class="cl"><span class="c1">// { name: &#34;external&#34;, size: 42 }
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">myFoo</span> <span class="o">=</span> <span class="nx">fooFactory</span><span class="p">.</span><span class="nx">make</span><span class="p">({</span> <span class="nx">size</span>: <span class="kt">20</span> <span class="p">});</span>
</span></span><span class="line"><span class="cl"><span class="c1">// { name: &#34;front&#34;, size: 20 }
</span></span></span></code></pre></div><p>The values can come from any function, so libraries such as Faker work naturally, but there is no
dependency on any particular data-generation tool.</p>
<h2 id="variant-factories">Variant factories</h2>
<p>Many test suites contain named variations of a common object. A system might have a normal user, an
administrator, a suspended account or a user with incomplete profile data. These are usually not
entirely different objects but are only modifications of a shared baseline.</p>
<p>VariantFactory provides that kind of variation on a baseline:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-typescript" data-lang="typescript"><span class="line"><span class="cl"><span class="kr">import</span> <span class="p">{</span> <span class="nx">DynamicFactory</span><span class="p">,</span> <span class="nx">VariantFactory</span> <span class="p">}</span> <span class="kr">from</span> <span class="s2">&#34;@kensio/part-factory&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="kr">import</span> <span class="p">{</span> <span class="nx">faker</span> <span class="p">}</span> <span class="kr">from</span> <span class="s2">&#34;@faker-js/faker&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">animalFactory</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">DynamicFactory</span><span class="p">&lt;</span><span class="nt">Foo</span><span class="p">&gt;(()</span> <span class="o">=&gt;</span> <span class="p">({</span>
</span></span><span class="line"><span class="cl">  <span class="nx">name</span><span class="o">:</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="nx">faker</span><span class="p">.</span><span class="nx">animal</span><span class="p">.</span><span class="kr">type</span><span class="p">(),</span>
</span></span><span class="line"><span class="cl">  <span class="nx">size</span><span class="o">:</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="nx">faker</span><span class="p">.</span><span class="kt">number</span><span class="p">.</span><span class="nx">int</span><span class="p">({</span> <span class="nx">max</span>: <span class="kt">100</span> <span class="p">}),</span>
</span></span><span class="line"><span class="cl"><span class="p">}));</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">zebraFactory</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">VariantFactory</span><span class="p">&lt;</span><span class="nt">Foo</span><span class="p">&gt;(</span><span class="nx">animalFactory</span><span class="p">,</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"><span class="nx">name</span><span class="o">:</span> <span class="s2">&#34;Zebra&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"><span class="p">});</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">zebra</span> <span class="o">=</span> <span class="nx">zebraFactory</span><span class="p">.</span><span class="nx">make</span><span class="p">();</span>
</span></span><span class="line"><span class="cl"><span class="c1">// { name: &#34;Zebra&#34;, size: 42 }
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">largeZebra</span> <span class="o">=</span> <span class="nx">zebraFactory</span><span class="p">.</span><span class="nx">make</span><span class="p">({</span> <span class="nx">size</span>: <span class="kt">100</span> <span class="p">});</span>
</span></span><span class="line"><span class="cl"><span class="c1">// { name: &#34;Zebra&#34;, size: 100 }
</span></span></span></code></pre></div><p>This allows common variations to become named concepts within the test suite without duplicating the
entire object definition. It also combines well in the situation described above where the baseline
object structure changes, because the variant factories don&rsquo;t need to be changed, only the shared
baseline.</p>
<h2 id="nested-overrides">Nested overrides</h2>
<p>Overrides are implemented as
<a href="https://github.com/KensioSoftware/part-factory/blob/main/src/deep-partial.ts">deep partials</a>. If a
test only needs to modify a single nested value, it does not need to replace the entire nested
object.</p>
<p>For example, if a user contains a nested address object, a test can override just the city while
keeping the rest of the default address intact. This is useful for API responses, configuration
objects, CMS data structures and other deeply nested data models.</p>
<h2 id="strong-typing">Strong typing</h2>
<p>The package is TypeScript-first, so factories understand the shape of the objects they produce,
allowing TypeScript to validate both default values and nested overrides.</p>
<p>Invalid property types are caught at compile time, and overrides cannot introduce properties that do
not exist on the target type. This helps keep test helpers aligned with application types rather
than becoming a loosely typed layer that gradually drifts away from the real data model.</p>
<h2 id="what-part-factory-is-not">What Part Factory is not</h2>
<p><em>Part Factory</em> is intentionally small in scope. It is not a fixture framework or fake data generator,
and it does not deal with object persistence or test runners.</p>
<p>All it does is create plain objects with default values and strongly typed nested overrides. More
advanced behaviour can always be built on top when required, but many unit and integration tests
only need a convenient way to create representative objects.</p>
<h2 id="installing-it">Installing it</h2>
<p>npm: <a href="https://www.npmjs.com/package/@kensio/part-factory">https://www.npmjs.com/package/@kensio/part-factory</a></p>
<p>GitHub: <a href="https://github.com/KensioSoftware/part-factory">https://github.com/KensioSoftware/part-factory</a></p>
]]></content:encoded><enclosure url="https://kensiosoftware.co.uk/blog/npm-kensio-part-factory-test-object-factory-package/npm-kensio-part-factory-test-object-factory-package-og.png" type="image/png"/><category>TypeScript &amp; JavaScript</category><category>Node.js</category></item><item><title>I passed the AWS Certified DevOps Engineer Professional exam</title><link>https://kensiosoftware.co.uk/blog/aws-certified-devops-engineer-professional-passed/</link><pubDate>Thu, 23 Apr 2026 18:00:00 +0000</pubDate><author>hugh@kensiosoftware.co.uk (Hugh Grigg)</author><guid>https://kensiosoftware.co.uk/blog/aws-certified-devops-engineer-professional-passed/</guid><description>I passed the AWS Certified DevOps Engineer – Professional exam, demonstrating my proficiency in automating, deploying, operating, and monitoring applications…</description><content:encoded><![CDATA[<p>I passed the AWS Certified DevOps Engineer Professional exam (DOP-C02) on 23rd April 2023 with a
score of 862/1000.</p>
<p>This certification focuses on knowledge of:</p>
<ul>
<li>AWS CodePipeline</li>
<li>AWS CodeBuild</li>
<li>AWS CodeDeploy</li>
<li>AWS CloudFormation</li>
<li>Amazon CloudWatch</li>
<li>AWS Systems Manager</li>
<li>AWS Identity and Access Management (IAM)</li>
<li>AWS Lambda</li>
<li>Amazon Elastic Container Service (ECS)</li>
<li>AWS Organizations</li>
</ul>
<p><a href="https://cp.certmetrics.com/amazon/en/public/verify/credential/54415a7ade114a47b2c7fa656a669be1">https://cp.certmetrics.com/amazon/en/public/verify/credential/54415a7ade114a47b2c7fa656a669be1</a></p>
<p><a href="hugh-grigg-aws-certified-devops-engineer-professional-54415a7ade114a47b2c7fa656a669be1.pdf">AWS Certified DevOps Engineer Professional (DOP-C02) Certificate PDF</a></p>
<p><img src="aws-certified-devops-engineer-professional.png" alt="AWS Certified DevOps Engineer Professional (DOP-C02)"></p>
]]></content:encoded><enclosure url="https://kensiosoftware.co.uk/blog/aws-certified-devops-engineer-professional-passed/aws-certified-devops-engineer-professional.png" type="image/png"/><category>AWS (Amazon Web Services)</category></item><item><title>I passed the AWS Certified Solutions Architect Professional exam</title><link>https://kensiosoftware.co.uk/blog/aws-certified-solutions-architect-professional-passed/</link><pubDate>Sun, 23 Apr 2023 18:00:00 +0000</pubDate><author>hugh@kensiosoftware.co.uk (Hugh Grigg)</author><guid>https://kensiosoftware.co.uk/blog/aws-certified-solutions-architect-professional-passed/</guid><description>I passed the AWS Certified Solutions Architect – Professional exam, demonstrating my proficiency in designing secure, scalable, resilient, and cost-effective…</description><content:encoded><![CDATA[<p>I passed the AWS Certified Solutions Architect Professional exam (SAP-C02) on 23rd April 2023 with a
score of 836/1000.</p>
<p>This certification focuses on knowledge of:</p>
<ul>
<li>Amazon VPC</li>
<li>AWS IAM</li>
<li>AWS Organizations</li>
<li>AWS Direct Connect</li>
<li>AWS Site-to-Site VPN</li>
<li>Amazon Route 53</li>
<li>Amazon EC2</li>
<li>Elastic Load Balancing</li>
<li>Amazon S3</li>
<li>Amazon RDS / Aurora</li>
<li>AWS CloudFormation</li>
</ul>
<p><a href="https://www.credly.com/earner/earned/badge/224c5eb1-956f-4b02-8170-72538488f8e0">https://www.credly.com/earner/earned/badge/224c5eb1-956f-4b02-8170-72538488f8e0</a></p>
<p><img src="aws-certified-solutions-architect-professional.png" alt="AWS Certified Solutions Architect Professional (SAP-C02)"></p>
]]></content:encoded><enclosure url="https://kensiosoftware.co.uk/blog/aws-certified-solutions-architect-professional-passed/aws-certified-solutions-architect-professional.png" type="image/png"/><category>AWS (Amazon Web Services)</category></item><item><title>I passed the AWS Certified Developer Associate exam</title><link>https://kensiosoftware.co.uk/blog/aws-certified-developer-associate-passed/</link><pubDate>Wed, 29 Mar 2023 18:00:00 +0000</pubDate><author>hugh@kensiosoftware.co.uk (Hugh Grigg)</author><guid>https://kensiosoftware.co.uk/blog/aws-certified-developer-associate-passed/</guid><description>I passed the AWS Certified Developer Associate exam, demonstrating my proficiency in developing applications on AWS. Certification…</description><content:encoded><![CDATA[<p>I passed the AWS Certified Developer Associate exam (DVA-C02) on 29th March 2023 with a score of
885/1000.</p>
<p>This certification focuses on knowledge of:</p>
<ul>
<li>AWS Lambda</li>
<li>Amazon DynamoDB</li>
<li>AWS IAM</li>
<li>Amazon API Gateway</li>
<li>Amazon SQS</li>
<li>Amazon S3</li>
<li>Amazon CloudWatch</li>
<li>Amazon SNS</li>
<li>Amazon EventBridge</li>
<li>AWS CodePipeline</li>
</ul>
<p><a href="https://www.credly.com/earner/earned/badge/4b2991ab-5568-4b2e-8a53-b73968c244b6">https://www.credly.com/earner/earned/badge/4b2991ab-5568-4b2e-8a53-b73968c244b6</a></p>
<p><img src="aws-certified-developer-associate.png" alt="AWS Certified Developer Associate (DVA-C02)"></p>
]]></content:encoded><enclosure url="https://kensiosoftware.co.uk/blog/aws-certified-developer-associate-passed/aws-certified-developer-associate.png" type="image/png"/><category>AWS (Amazon Web Services)</category></item><item><title>Automated static site infrastructure with AWS CloudFormation, Cloudfront, S3, Hugo, GitHub Actions</title><link>https://kensiosoftware.co.uk/blog/aws-cloudformation-github-action-hugo-static-site/</link><pubDate>Tue, 15 Mar 2022 15:09:13 +0000</pubDate><author>hugh@kensiosoftware.co.uk (Hugh Grigg)</author><guid>https://kensiosoftware.co.uk/blog/aws-cloudformation-github-action-hugo-static-site/</guid><description>After manually setting up several static sites with AWS, Hugo and GitHub Actions, I got round to putting together CloudFormation configuration to do it…</description><content:encoded><![CDATA[<p>After manually setting up several
<a href="https://notestoself.dev/posts/deploy-hugo-site-github-actions-s3-cloudfront-aws-iam/">static sites with AWS, Hugo and GitHub Actions</a>,
I got round to putting together CloudFormation configuration to do it
automatically.</p>
<p>This includes handling the TLS certificate in AWS ACM. Unfortunately it seems
it&rsquo;s either difficult or impossible to do this in a single CloudFormation
stack, because the ACM certificate must be created in the <code>us-east-1</code> region,
and there&rsquo;s no straightforward way to then depend on and access that resource
for resources being deployed in another region (<code>eu-west-2</code> in my case).</p>
<p>Because of that, I&rsquo;ve split it into two separate CloudFormation stacks, one that
handles the Hosted Zone in Route53 and the ACM Certificate for it, which is
deployed in <code>us-east-1</code>, and then another stack for everything else, which is
deployed in <code>eu-west-2</code>.</p>
<p>The CloudFormation template for the Route 53 Hosted Zone and ACM Certificate
looks like this:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">AWSTemplateFormatVersion</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;2010-09-09&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">Description</span><span class="p">:</span><span class="w"> </span><span class="l">Hosted Zone and ACM Certificate for static website.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c"># NOTE: If the domain name is not registered in Route 53, you&#39;ll need to begin</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c"># the creation of this stack, wait for the Route53 Hosted Zone to be created,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c"># then go and take the name servers for it in Route 53 and set them in the</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c"># registrar (e.g. Namecheap) so that DNS validation of the ACM Certificate can</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c"># complete. It can take more than 30 minutes for the validation to happen after</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c"># this.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">Parameters</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">DomainName</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">Type</span><span class="p">:</span><span class="w"> </span><span class="l">String</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">Description</span><span class="p">:</span><span class="w"> </span><span class="l">The domain name for the static site.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">Resources</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">Route53HostedZone</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">Type</span><span class="p">:</span><span class="w"> </span><span class="l">AWS::Route53::HostedZone</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">Properties</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">Name</span><span class="p">:</span><span class="w"> </span>!<span class="l">Ref DomainName</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">DomainCertificate</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">Type</span><span class="p">:</span><span class="w"> </span><span class="l">AWS::CertificateManager::Certificate</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">Properties</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">DomainName</span><span class="p">:</span><span class="w"> </span>!<span class="l">Ref DomainName</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">ValidationMethod</span><span class="p">:</span><span class="w"> </span><span class="l">DNS</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">DomainValidationOptions</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span>- <span class="nt">DomainName</span><span class="p">:</span><span class="w"> </span>!<span class="l">Ref DomainName</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">HostedZoneId</span><span class="p">:</span><span class="w"> </span>!<span class="l">Ref Route53HostedZone</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">Outputs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">CertificateArn</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">Value</span><span class="p">:</span><span class="w"> </span>!<span class="l">Ref DomainCertificate</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">Description</span><span class="p">:</span><span class="w"> </span><span class="l">ARN of ACM Certificate created inside StackSet.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">Export</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">Name</span><span class="p">:</span><span class="w"> </span>!<span class="l">Sub ${AWS::StackName}-cert-arn</span><span class="w">
</span></span></span></code></pre></div><p>You can deploy that with your choice of domain name like this:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">export</span> <span class="nv">DOMAIN_NAME</span><span class="o">=</span><span class="s1">&#39;example.com&#39;</span> <span class="o">&amp;&amp;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="nb">export</span> <span class="nv">ZONE_CERT_STACK</span><span class="o">=</span><span class="s1">&#39;foobar-zone-cert-stack-name&#39;</span> <span class="o">&amp;&amp;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">aws cloudformation create-stack <span class="se">\
</span></span></span><span class="line"><span class="cl">  --region <span class="s1">&#39;us-east-1&#39;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  --stack-name <span class="s2">&#34;</span><span class="si">${</span><span class="nv">ZONE_CERT_STACK</span><span class="si">}</span><span class="s2">&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  --template-body file://path/to/domain-certificate.cloudformation.yml <span class="se">\
</span></span></span><span class="line"><span class="cl">  --parameters <span class="nv">ParameterKey</span><span class="o">=</span>DomainName,ParameterValue<span class="o">=</span><span class="s2">&#34;</span><span class="si">${</span><span class="nv">DOMAIN_NAME</span><span class="si">}</span><span class="s2">&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  <span class="o">&amp;&amp;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">aws cloudformation --region <span class="s1">&#39;us-east-1&#39;</span> <span class="nb">wait</span> stack-create-complete --stack-name <span class="s2">&#34;</span><span class="si">${</span><span class="nv">ZONE_CERT_STACK</span><span class="si">}</span><span class="s2">&#34;</span> <span class="o">&amp;&amp;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">aws cloudformation --region <span class="s1">&#39;us-east-1&#39;</span> describe-stacks --stack-name <span class="s2">&#34;</span><span class="si">${</span><span class="nv">ZONE_CERT_STACK</span><span class="si">}</span><span class="s2">&#34;</span>
</span></span></code></pre></div><p>Note that if the domain name is not registered in Route 53, you&rsquo;ll need to begin
the creation of this stack, wait for the Route53 Hosted Zone to be created,
then go and take the name servers for it in Route 53 and set them in the
registrar (e.g. Namecheap) so that DNS validation of the ACM Certificate can
complete. It can take more than 30 minutes for the validation to happen after
this, because it has to wait for DNS to update.</p>
<p>Once that stack has deployed, you can find the ARN of the new ACM Certificate
like this:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">aws acm list-certificates --region <span class="s1">&#39;us-east-1&#39;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  --query <span class="s1">&#39;CertificateSummaryList[].[CertificateArn,DomainName]&#39;</span> --output text <span class="se">\
</span></span></span><span class="line"><span class="cl">  <span class="p">|</span> grep <span class="s2">&#34;</span><span class="si">${</span><span class="nv">DOMAIN_NAME</span><span class="si">}</span><span class="s2">&#34;</span>
</span></span></code></pre></div><p>You can manually store that Certificate ARN in an environment variable, or do it
automatically in one command:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">export</span> <span class="nv">CERT_ARN</span><span class="o">=</span><span class="k">$(</span> aws acm list-certificates --region <span class="s1">&#39;us-east-1&#39;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  --query <span class="s1">&#39;CertificateSummaryList[].[CertificateArn,DomainName]&#39;</span> --output text <span class="se">\
</span></span></span><span class="line"><span class="cl">  <span class="p">|</span> grep <span class="s2">&#34;</span><span class="si">${</span><span class="nv">DOMAIN_NAME</span><span class="si">}</span><span class="s2">&#34;</span> <span class="p">|</span> cut -f1 <span class="k">)</span>
</span></span></code></pre></div><p>Then we use the main CloudFormation template to set up all the other AWS
infrastructure for the static site. The template looks like this:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">AWSTemplateFormatVersion</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;2010-09-09&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">Description</span><span class="p">:</span><span class="w"> </span><span class="l">Static website set up.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">Parameters</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">DomainName</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">Type</span><span class="p">:</span><span class="w"> </span><span class="l">String</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">Description</span><span class="p">:</span><span class="w"> </span><span class="l">The domain name for the static site.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">CertificateArn</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">Type</span><span class="p">:</span><span class="w"> </span><span class="l">String</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">Description</span><span class="p">:</span><span class="w"> </span><span class="l">ARN of the ACM Certificate.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">Mappings</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">Region2S3WebsiteSuffix</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">us-east-1</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">Suffix</span><span class="p">:</span><span class="w"> </span><span class="l">.s3-website-us-east-1.amazonaws.com</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">us-west-1</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">Suffix</span><span class="p">:</span><span class="w"> </span><span class="l">.s3-website-us-west-1.amazonaws.com</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">us-west-2</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">Suffix</span><span class="p">:</span><span class="w"> </span><span class="l">.s3-website-us-west-2.amazonaws.com</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">eu-west-1</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">Suffix</span><span class="p">:</span><span class="w"> </span><span class="l">.s3-website-eu-west-1.amazonaws.com</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">ap-northeast-1</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">Suffix</span><span class="p">:</span><span class="w"> </span><span class="l">.s3-website-ap-northeast-1.amazonaws.com</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">ap-northeast-2</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">Suffix</span><span class="p">:</span><span class="w"> </span><span class="l">.s3-website-ap-northeast-2.amazonaws.com</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">ap-southeast-1</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">Suffix</span><span class="p">:</span><span class="w"> </span><span class="l">.s3-website-ap-southeast-1.amazonaws.com</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">ap-southeast-2</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">Suffix</span><span class="p">:</span><span class="w"> </span><span class="l">.s3-website-ap-southeast-2.amazonaws.com</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">ap-south-1</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">Suffix</span><span class="p">:</span><span class="w"> </span><span class="l">.s3-website-ap-south-1.amazonaws.com</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">us-east-2</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">Suffix</span><span class="p">:</span><span class="w"> </span><span class="l">.s3-website-us-east-2.amazonaws.com</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">sa-east-1</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">Suffix</span><span class="p">:</span><span class="w"> </span><span class="l">.s3-website-sa-east-1.amazonaws.com</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">cn-north-1</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">Suffix</span><span class="p">:</span><span class="w"> </span><span class="l">.s3-website.cn-north-1.amazonaws.com.cn</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">eu-central-1</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">Suffix</span><span class="p">:</span><span class="w"> </span><span class="l">.s3-website.eu-central-1.amazonaws.com</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">eu-west-2</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">Suffix</span><span class="p">:</span><span class="w"> </span><span class="l">.s3-website.eu-west-2.amazonaws.com</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">Resources</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">WebsiteDNSName</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">Type</span><span class="p">:</span><span class="w"> </span><span class="l">AWS::Route53::RecordSet</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">Properties</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">HostedZoneName</span><span class="p">:</span><span class="w"> </span>!<span class="l">Join [ &#39;&#39;, [ !Ref DomainName, . ] ]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">Comment</span><span class="p">:</span><span class="w"> </span><span class="l">CNAME redirect custom name to CloudFront distribution</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">Name</span><span class="p">:</span><span class="w"> </span>!<span class="l">Ref DomainName</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">Type</span><span class="p">:</span><span class="w"> </span><span class="l">A</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">AliasTarget</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      	</span><span class="c"># https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-route53-aliastarget.html</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">HostedZoneId</span><span class="p">:</span><span class="w"> </span><span class="l">Z2FDTNDATAQYW2</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">DNSName</span><span class="p">:</span><span class="w"> </span>!<span class="l">GetAtt WebsiteCDN.DomainName</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">S3BucketForWebsiteContent</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">Type</span><span class="p">:</span><span class="w"> </span><span class="l">AWS::S3::Bucket</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">Properties</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">BucketName</span><span class="p">:</span><span class="w"> </span>!<span class="l">Ref DomainName</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">AccessControl</span><span class="p">:</span><span class="w"> </span><span class="l">PublicRead</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">WebsiteConfiguration</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">IndexDocument</span><span class="p">:</span><span class="w"> </span><span class="l">index.html</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">ErrorDocument</span><span class="p">:</span><span class="w"> </span><span class="l">error.html</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">WebsiteCDN</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">Type</span><span class="p">:</span><span class="w"> </span><span class="l">AWS::CloudFront::Distribution</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">Properties</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">DistributionConfig</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">Comment</span><span class="p">:</span><span class="w"> </span>!<span class="l">Ref DomainName</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">Aliases</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span>- !<span class="l">Ref DomainName</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">Enabled</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">PriceClass</span><span class="p">:</span><span class="w"> </span><span class="l">PriceClass_100</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">HttpVersion</span><span class="p">:</span><span class="w"> </span><span class="l">http2</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">DefaultCacheBehavior</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">ViewerProtocolPolicy</span><span class="p">:</span><span class="w"> </span><span class="l">redirect-to-https</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="c"># https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/using-managed-cache-policies.html</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">CachePolicyId</span><span class="p">:</span><span class="w"> </span><span class="l">658327ea-f89d-4fab-a63d-7e88639e58f6</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">TargetOriginId</span><span class="p">:</span><span class="w"> </span><span class="l">only-origin</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">Compress</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">Origins</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span>- <span class="nt">CustomOriginConfig</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">OriginProtocolPolicy</span><span class="p">:</span><span class="w"> </span><span class="l">http-only</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">DomainName</span><span class="p">:</span><span class="w"> </span>!<span class="l">Join [&#39;&#39;,[</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span>!<span class="l">Ref &#39;S3BucketForWebsiteContent&#39;,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span>!<span class="l">FindInMap [Region2S3WebsiteSuffix,!Ref &#39;AWS::Region&#39;, Suffix]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="p">]]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">Id</span><span class="p">:</span><span class="w"> </span><span class="l">only-origin</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">CNAMEs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span>- !<span class="l">Ref DomainName</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">ViewerCertificate</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">AcmCertificateArn</span><span class="p">:</span><span class="w"> </span>!<span class="l">Ref CertificateArn</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">MinimumProtocolVersion</span><span class="p">:</span><span class="w"> </span><span class="l">TLSv1.2_2019</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">SslSupportMethod</span><span class="p">:</span><span class="w"> </span><span class="l">sni-only</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">UpdateSiteUser</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">Type</span><span class="p">:</span><span class="w"> </span><span class="l">AWS::IAM::User</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">UpdateSiteUserAccessKey</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">Type</span><span class="p">:</span><span class="w"> </span><span class="l">AWS::IAM::AccessKey</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">Properties</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">UserName</span><span class="p">:</span><span class="w"> </span>!<span class="l">Ref UpdateSiteUser</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">UpdateSiteUserAccessKeySecret</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">Type</span><span class="p">:</span><span class="w"> </span><span class="l">AWS::SecretsManager::Secret</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">Properties</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">Name</span><span class="p">:</span><span class="w"> </span>!<span class="l">Sub ${AWS::StackName}-update-site-user-access-key-secret</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">Description</span><span class="p">:</span><span class="w"> </span>!<span class="l">Sub &#34;Access key credentials for ${AWS::StackName} site update user.&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">SecretString</span><span class="p">:</span><span class="w"> </span>!<span class="l">Sub</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span>- <span class="s1">&#39;{&#34;AccessKeyId&#34;:&#34;${AccessKeyId}&#34;,&#34;SecretAccessKey&#34;:&#34;${SecretAccessKey}&#34;}&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span>- <span class="nt">AccessKeyId</span><span class="p">:</span><span class="w"> </span>!<span class="l">Ref UpdateSiteUserAccessKey</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">SecretAccessKey</span><span class="p">:</span><span class="w"> </span>!<span class="l">GetAtt UpdateSiteUserAccessKey.SecretAccessKey</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">UpdateSitePolicy</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">Type</span><span class="p">:</span><span class="w"> </span><span class="l">AWS::IAM::Policy</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">Properties</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">PolicyName</span><span class="p">:</span><span class="w"> </span>!<span class="l">Sub update-${AWS::StackName}-site-policy</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">PolicyDocument</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">Version</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;2012-10-17&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">Statement</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span>- <span class="nt">Effect</span><span class="p">:</span><span class="w"> </span><span class="l">Allow</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">Action</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span>- <span class="l">s3:PutObject</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span>- <span class="l">s3:PutBucketPolicy</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span>- <span class="l">s3:ListBucket</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span>- <span class="l">s3:DeleteObject</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span>- <span class="l">s3:PutObjectAcl</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span>- <span class="l">cloudfront:CreateInvalidation</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span>- <span class="l">s3:GetBucketPolicy</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">Resource</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span>- !<span class="l">Sub</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                </span>- <span class="s2">&#34;arn:aws:cloudfront::${AWS::AccountId}:distribution/${WebsiteCDN}&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                </span>- <span class="nt">WebsiteCDN</span><span class="p">:</span><span class="w"> </span>!<span class="l">Ref WebsiteCDN</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span>- !<span class="l">GetAtt S3BucketForWebsiteContent.Arn</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span>- !<span class="l">Join [&#34;&#34;, [!GetAtt S3BucketForWebsiteContent.Arn, &#34;/*&#34;]]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">Users</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span>- !<span class="l">Ref UpdateSiteUser</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">Outputs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">BucketName</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">Description</span><span class="p">:</span><span class="w"> </span><span class="l">Name of S3 bucket for website content.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">Value</span><span class="p">:</span><span class="w"> </span>!<span class="l">Ref S3BucketForWebsiteContent</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">CloudfrontDistribution</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">Description</span><span class="p">:</span><span class="w"> </span><span class="l">CloudFront distribution ID.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">Value</span><span class="p">:</span><span class="w"> </span>!<span class="l">Ref WebsiteCDN</span><span class="w">
</span></span></span></code></pre></div><p>Note that for the next step, the <code>CERT_ARN</code> environment variable needs to be set
as described above. You can then deploy a stack from that template like this:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">export</span> <span class="nv">DOMAIN_NAME</span><span class="o">=</span><span class="s1">&#39;example.com&#39;</span> <span class="o">&amp;&amp;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="nb">export</span> <span class="nv">MAIN_STACK</span><span class="o">=</span><span class="s1">&#39;foobar-stack-name&#39;</span> <span class="o">&amp;&amp;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">aws cloudformation --region <span class="s1">&#39;eu-west-2&#39;</span> create-stack <span class="se">\
</span></span></span><span class="line"><span class="cl">  --stack-name <span class="s2">&#34;</span><span class="si">${</span><span class="nv">MAIN_STACK</span><span class="si">}</span><span class="s2">&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  --template-body file://path/to/cloudformation.yml <span class="se">\
</span></span></span><span class="line"><span class="cl">  --capabilities CAPABILITY_IAM <span class="se">\
</span></span></span><span class="line"><span class="cl">  --parameters <span class="nv">ParameterKey</span><span class="o">=</span>DomainName,ParameterValue<span class="o">=</span><span class="s2">&#34;</span><span class="si">${</span><span class="nv">DOMAIN_NAME</span><span class="si">}</span><span class="s2">&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">               <span class="nv">ParameterKey</span><span class="o">=</span>CertificateArn,ParameterValue<span class="o">=</span><span class="s2">&#34;</span><span class="si">${</span><span class="nv">CERT_ARN</span><span class="si">}</span><span class="s2">&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  <span class="o">&amp;&amp;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">aws cloudformation --region <span class="s1">&#39;eu-west-2&#39;</span> <span class="nb">wait</span> stack-create-complete --stack-name <span class="s2">&#34;</span><span class="si">${</span><span class="nv">MAIN_STACK</span><span class="si">}</span><span class="s2">&#34;</span> <span class="o">&amp;&amp;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">aws cloudformation --region <span class="s1">&#39;eu-west-2&#39;</span> describe-stacks --stack-name <span class="s2">&#34;</span><span class="si">${</span><span class="nv">MAIN_STACK</span><span class="si">}</span><span class="s2">&#34;</span>
</span></span></code></pre></div><p>That stack includes an IAM user with granular permissions for updating the
static site, and an access key for that user. To get the access key id and
secret access key for the site update user:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">export</span> <span class="nv">MAIN_STACK</span><span class="o">=</span><span class="s1">&#39;foobar-stack-name&#39;</span> <span class="o">&amp;&amp;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">aws secretsmanager get-secret-value --secret-id <span class="s2">&#34;</span><span class="si">${</span><span class="nv">MAIN_STACK</span><span class="si">}</span><span class="s2">-update-site-user-access-key-secret&#34;</span>
</span></span></code></pre></div><p>Take those access key credentials and use them set two secrets in the GitHub
repo for the project: <code>AWS_ACCESS_KEY_ID</code> and <code>AWS_SECRET_ACCESS_KEY</code>.</p>
<p>After that, add a GitHub Action workflow in the project at
<code>.github/workflows/update-site.yml</code>. The content of the GitHub Action file is:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">Update site</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">on</span><span class="p">:</span><span class="w"> </span><span class="l">push</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">jobs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">build</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">Build and Deploy</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">runs-on</span><span class="p">:</span><span class="w"> </span><span class="l">ubuntu-latest</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">steps</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">uses</span><span class="p">:</span><span class="w"> </span><span class="l">actions/checkout@v1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">Install Hugo</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">run</span><span class="p">:</span><span class="w"> </span><span class="p">|</span><span class="sd">
</span></span></span><span class="line"><span class="cl"><span class="sd">          HUGO_DOWNLOAD=hugo_extended_${HUGO_VERSION}_Linux-64bit.tar.gz
</span></span></span><span class="line"><span class="cl"><span class="sd">          wget https://github.com/gohugoio/hugo/releases/download/v${HUGO_VERSION}/${HUGO_DOWNLOAD}
</span></span></span><span class="line"><span class="cl"><span class="sd">          tar xvzf ${HUGO_DOWNLOAD} hugo
</span></span></span><span class="line"><span class="cl"><span class="sd">          mv hugo $HOME/hugo</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">env</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">HUGO_VERSION</span><span class="p">:</span><span class="w"> </span><span class="m">0.94.2</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">Use Node.js</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">uses</span><span class="p">:</span><span class="w"> </span><span class="l">actions/setup-node@v1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">with</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">node-version</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;16.x&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">NPM install</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">run</span><span class="p">:</span><span class="w"> </span><span class="p">|</span><span class="sd">
</span></span></span><span class="line"><span class="cl"><span class="sd">          npm install
</span></span></span><span class="line"><span class="cl"><span class="sd">          npm install -g postcss-cli autoprefixer</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">Hugo Build</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">run</span><span class="p">:</span><span class="w"> </span><span class="l">$HOME/hugo -v</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">Deploy to S3</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">if</span><span class="p">:</span><span class="w"> </span><span class="l">github.ref == &#39;refs/heads/main&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">run</span><span class="p">:</span><span class="w"> </span><span class="l">aws s3 sync public/ s3://example.com/ --delete --acl=public-read &amp;&amp; aws cloudfront create-invalidation --distribution-id=ABC123CFDISTID --paths=/*</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">env</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">AWS_ACCESS_KEY_ID</span><span class="p">:</span><span class="w"> </span><span class="l">${{ secrets.AWS_ACCESS_KEY_ID }}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">AWS_SECRET_ACCESS_KEY</span><span class="p">:</span><span class="w"> </span><span class="l">${{ secrets.AWS_SECRET_ACCESS_KEY }}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">AWS_EC2_METADATA_DISABLED</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span></code></pre></div><p>Note that you need to set the S3 bucket name and CloudFront Distribution ID
in the GitHub Action file.</p>
<p>You should now have push-to-deploy with the static site content being generated
in the Github Action and then pushed to S3.</p>
<p>If you need any help with <a href="https://kensiosoftware.co.uk/freelance-aws-developer/">AWS</a> development work, then
<a href="/contact/">contact me</a>.</p>
]]></content:encoded><enclosure url="https://kensiosoftware.co.uk/blog/aws-cloudformation-github-action-hugo-static-site/aws-cloudformation-hugo-static-site.jpg" type="image/png"/><category>AWS (Amazon Web Services)</category><category>Serverless Systems</category></item><item><title>Working as a software contractor has confirmed the value of "excessive" tests to me</title><link>https://kensiosoftware.co.uk/blog/software-contractor-confirm-value-excessive-tests/</link><pubDate>Tue, 16 Nov 2021 00:00:00 +0000</pubDate><author>hugh@kensiosoftware.co.uk (Hugh Grigg)</author><guid>https://kensiosoftware.co.uk/blog/software-contractor-confirm-value-excessive-tests/</guid><description>During my career as a software engineer so far, I’ve tended to err on the side of caution when writing tests, taking the view that “more is more”. I think…</description><content:encoded><![CDATA[<p>During my career as a software engineer so far, I&rsquo;ve tended to err on the side
of caution when writing tests, taking the view that &ldquo;more is more&rdquo;. I think
it&rsquo;s usually better to have one more test than not to have it, even though
there is of course a cost to each extra test. Sometimes this attitude results in
tests that I admit might be a bit excessive in terms of what they cover.</p>
<p>I worked in permanent roles at various start ups in London for about eight
years, and in 2021 I made the switch to contracting and freelancing. I knew
that as a contractor I would be thrown into all sorts of projects with
different levels of design, development and maintenance. One thing that has
struck me from taking on this kind of work is that I actually feel a
vindication of the &ldquo;excessive&rdquo; testing described above.</p>
<p>For example, in a permanent role in the past, I developed a system involving an
XML message processor which needed to verify XML signatures on the incoming
messages. At the time, I wrote tests that generated a fresh private key and
certificate, signed an XML message with the key, configured the system under
test to use the certificate, and confirmed that it could verify the signed XML
and also perform whatever business logic was being covered in each particular
test case. A lot of this might seem a bit excessive or unnecessary for testing
a fairly bog standard system.</p>
<p>For example, it might have been easier to not bother with the XML signature and
verification in tests, as that part was mostly just calls to a library anyway.
Alternatively, it might have been easier to just generate a test private key
and certificate manually and perhaps keep it in a password manager somewhere or
even commit it into the repo as it&rsquo;s only for testing purposes.</p>
<p>However, having the test code generate fresh keys and certificates to use on the
fly does have some benefits. It exercises more of the system from start to end.
The tests confirm the configuration of the system and its use of the library,
as well as the business logic. If we need to upgrade the library or switch to
another one, the tests will be able to validate that change with quite a lot of
confidence. Having the key and certificate generation code committed in the
repo is also a form of documentation of how that works, and makes it easier to
modify the tests in future if we switch to a different signing algorithm or key
strength. It also means the tests &ldquo;just work&rdquo;, with no set up required other
than running the tests.</p>
<p>With more detailed and extensive tests, it&rsquo;s less mysterious what is happening
in the system and in the tests, and that can be useful for people working on
both of those in the future. It can save money for the company, because future
development can happen quickly, confidently and safely by leaning on these
kinds of tests.</p>
<p>Coming back to the contracting work, the above was somewhat vindicated recently
when I worked on a similar system involving XML messages with signatures. In
this case, whoever built the system in the past hadn&rsquo;t included tests that
covered the signature verification part of the system. Lo and behold, by the
time I came in to work on this project, the changes that were needed involved
modifying the signature verification as well as how the XML structure was
parsed. It took a lot of digging around in sparse documentation, trawling
through old commits and tracking down people who might have some more
knowledge. This was the only way to gather all of the necessary information to
make the changes safely and then deploy them with confidence. A lot of the time
and effort could have been saved had more extensive tests been put in at the
time of development.</p>
<p>In summary, I&rsquo;d say this confirms that it&rsquo;s always worth considering other
people or your future self when deciding how detailed and extensive to make
your test cases. More often than not, it will pay off to cover more of the
system with explicit tests.</p>
]]></content:encoded><enclosure url="https://kensiosoftware.co.uk/blog/software-contractor-confirm-value-excessive-tests/xml-code-blurred.png" type="image/png"/></item><item><title>Freelancing for Doozy</title><link>https://kensiosoftware.co.uk/blog/doozy-freelancing/</link><pubDate>Fri, 08 Oct 2021 00:00:00 +0000</pubDate><author>hugh@kensiosoftware.co.uk (Hugh Grigg)</author><guid>https://kensiosoftware.co.uk/blog/doozy-freelancing/</guid><description>I’ve been helping to build Doozy’s product as a freelance Software Engineer, using GCP, Firebase, TypeScript and serverless technology. I’m always keen to…</description><content:encoded><![CDATA[<p>I&rsquo;ve been helping to build <a href="https://doozy.live/">Doozy&rsquo;s product</a> as a freelance
Software Engineer, using GCP, Firebase, TypeScript and serverless technology.</p>
<figure>
<img
src='/blog/doozy-freelancing/doozy.png'
alt='Screenshot of Doozy&#39;s website'
title='Screenshot of Doozy&#39;s website'
>
</figure>
<p>I&rsquo;m always keen to help early stage start ups with building an MVP, so
<a href="/contact">contact me</a> and we can discuss working together.</p>
]]></content:encoded><enclosure url="https://kensiosoftware.co.uk/blog/doozy-freelancing/doozy.png" type="image/png"/><category>Firebase</category><category>GCP (Google Cloud Platform)</category><category>TypeScript &amp; JavaScript</category><category>Serverless Systems</category></item><item><title>Contracting for Bulb via YLD</title><link>https://kensiosoftware.co.uk/blog/bulb-yld-contracting/</link><pubDate>Fri, 01 Oct 2021 00:00:00 +0000</pubDate><author>hugh@kensiosoftware.co.uk (Hugh Grigg)</author><guid>https://kensiosoftware.co.uk/blog/bulb-yld-contracting/</guid><description>I’ve been providing software engineering services to Bulb via YLD, using Kubernetes, Terraform, GCP, TypeScript, Node.js, Python, MySQL, PostgreSQL and other…</description><content:encoded><![CDATA[<p>I&rsquo;ve been providing software engineering services to Bulb via YLD, using
Kubernetes, <a href="https://kensiosoftware.co.uk/freelance-terraform-developer/">Terraform</a>, <a href="https://kensiosoftware.co.uk/freelance-gcp-google-cloud-platform-developer/">GCP</a>,
<a href="https://kensiosoftware.co.uk/freelance-javascript-typescript-developer/">TypeScript</a>, <a href="https://kensiosoftware.co.uk/freelance-node.js-developer/">Node.js</a>,
Python, MySQL, <a href="https://kensiosoftware.co.uk/freelance-postgresql-developer/">PostgreSQL</a> and other technologies.</p>
<figure>
<img
src='/blog/bulb-yld-contracting/bulb.png'
alt='Screenshot of Bulb&#39;s website'
title='Screenshot of Bulb&#39;s website'
>
</figure>
<p>If you need an experienced contract software engineer in the UK on a remote
basis, please <a href="/contact">get in touch</a>.</p>
]]></content:encoded><enclosure url="https://kensiosoftware.co.uk/blog/bulb-yld-contracting/bulb.png" type="image/png"/><category>GCP (Google Cloud Platform)</category><category>TypeScript &amp; JavaScript</category><category>MySQL</category><category>PostgreSQL</category><category>Python</category><category>Terraform</category></item><item><title>What is the biggest tradeoff of going serverless?</title><link>https://kensiosoftware.co.uk/blog/what-is-the-biggest-tradeoff-of-going-serverless/</link><pubDate>Fri, 06 Aug 2021 00:00:00 +0000</pubDate><author>hugh@kensiosoftware.co.uk (Hugh Grigg)</author><guid>https://kensiosoftware.co.uk/blog/what-is-the-biggest-tradeoff-of-going-serverless/</guid><description>There was an interesting Twitter thread yesterday about the tradeoffs of going serverless. There are indeed many such tradeoffs. Serverless approaches offer…</description><content:encoded><![CDATA[<p>There was an interesting Twitter thread yesterday about the
<a href="https://twitter.com/brianleroux/status/1422659956571099137">tradeoffs of going serverless</a>.</p>
<p>There are indeed many such tradeoffs. Serverless approaches offer many benefits,
in particular the potential for cost savings (at least in the early stages of a
project&rsquo;s lifecycle) and the chance to offload what would otherwise be a lot of
operational management work on to the cloud provider, letting you focus on your
building your business and not an
<a href="https://notestoself.dev/posts/abstraction-castles/">abstraction castle</a>.</p>
<h2 id="potential-for-greater-complexity">Potential for greater complexity</h2>
<figure>
<img
src='/blog/what-is-the-biggest-tradeoff-of-going-serverless/complex-serverless-cloud-architecture.png'
alt='A complex serverless cloud architecture diagram'
title='A complex serverless cloud architecture diagram'
>
</figure>
<p>This isn&rsquo;t even an exaggerration of the complexity of most serverless and cloud
architectures I have worked with. Cloud providers like AWS and GCP make it very
easy to add ever more nodes to the architecture diagram, as they are more than
happy to charge you for all of these managed services. This needs to be balanced
against the potential for cost-savings by handling the fundamentals yourself,
and of course the eternal benefits of <strong>keeping things simple</strong>.</p>
<h2 id="lack-of-observability--lack-of-ability-to-audit">Lack of observability / lack of ability to audit</h2>
<p>Because serverless approaches can make it easy to continually add more and more
individually deployed functions, there can be a combinatorial explosion of
potential execution paths through the distributed system as the functions
interact with each other in myriad ways. This can make it difficult to observe
what is happening at the logical level, or to track down bugs hiding amongst
obscure edges in the execution graph.</p>
<p>This point might be more debatable than the rest, as there is an argument that you
get better observability with serverless architectures because you can easily
separate out compute, storage, running costs and so on down to the function
level.</p>
<h2 id="database-connection-count">Database connection count</h2>
<p>Pretty much every company that uses serverless is going to hit the max database
connections issue. With every function invocation in its own container demanding
its own database connection, its easy to exceed the maximum number of concurrent
connections that the database can handle.</p>
<p>Having said that, this was already a problem with elastic autoscaling servers
in the cloud &ndash; it&rsquo;s perfectly possible to scale up enough servers to exceed
your database&rsquo;s max connections limit.</p>
<p>There are various solutions to this, including database connection pooling,
CQRS, read-replicas and so on.</p>
<h2 id="vendor-lock-in">Vendor lock-in</h2>
<p>Another common objection to serverless approaches is the high potential for
vendor lock in, as you&rsquo;re tied into the serverless technology of whichever
cloud platform you use. Cloud providers tend to encourage this by pushing
features such as vendor-specific function triggers that tend to rely on use of
their SDK to be convenient.</p>
<p>If you treat serverless as more of a deployment strategy than an architectural
paradigm, you might be able to mitigate this issue to some extent by depending
less on vendor-specific offerings, or at least depending on them less directly.</p>
]]></content:encoded><enclosure url="https://kensiosoftware.co.uk/blog/what-is-the-biggest-tradeoff-of-going-serverless/complex-serverless-cloud-architecture.png" type="image/png"/><category>Serverless Systems</category></item><item><title>Custom e-commerce search development for Moment Wall Art</title><link>https://kensiosoftware.co.uk/blog/custom-e-commerce-search-development-for-moment-wall-art/</link><pubDate>Sun, 01 Nov 2020 00:00:00 +0000</pubDate><author>hugh@kensiosoftware.co.uk (Hugh Grigg)</author><guid>https://kensiosoftware.co.uk/blog/custom-e-commerce-search-development-for-moment-wall-art/</guid><description>Moment Wall Art needed a fast and efficient e-commerce search solution for their online sales system. I was able to provide a scalable, low-cost search…</description><content:encoded><![CDATA[<p><a href="https://momentwallart.co.uk/">Moment Wall Art</a> needed a fast and efficient
e-commerce search solution for their online sales system. I was able to provide
a scalable, low-cost search solution with a quick turn around.</p>
<figure>
<img
src='/blog/custom-e-commerce-search-development-for-moment-wall-art/moment-wall-art-custom-e-commerce-search-development.jpg'
alt='Moment Wall Art E-commerce Search Development'
title='Moment Wall Art E-commerce Search Development'
>
</figure>
<p>You can see the search system at Moment Wall Art, for example by searching
for
<a href="https://momentwallart.co.uk/search/?q=Japanese+river" title="Japanese River Wall Art">&ldquo;Japanese river&rdquo;</a>.</p>
<p>The <a href="https://kensiosoftware.co.uk/freelance-e-commerce-development/">e-commerce sales system</a> is primarily a static
website to ensure high scalability and fast response times, with low operating
costs. The new search solution builds on top of this static system by
pre-building indexes of the product data into individual JSON files that are
separated by keyword. These keyword-based JSON index files are deployed
alongside the rest of the e-commerce website in AWS S3 with CloudFront in front
of them as a distributed caching layer.</p>
<p>When a customer searches for art pieces to buy on Moment Wall Art, a small piece
of vanilla JavaScript code fetches the relevant JSON index file for each
keyword, each of which contains optimised product data relevant to their
respective keyword.</p>
<p>The product data in the keyword index files is then aggregated and rendered to
the page for the customer to browse through.</p>
<p>Due to the front end optimisation and static back end powering this solution,
search result pages are highly responsive, and the solution has low operating
costs.</p>
<p><a href="/contact">Contact me</a> about custom search solution development.</p>
]]></content:encoded><enclosure url="https://kensiosoftware.co.uk/blog/custom-e-commerce-search-development-for-moment-wall-art/moment-wall-art-custom-e-commerce-search-development.jpg" type="image/png"/><category>AWS (Amazon Web Services)</category><category>TypeScript &amp; JavaScript</category></item><item><title>Etsy integration for Moment Wall Art</title><link>https://kensiosoftware.co.uk/blog/etsy-integration-for-moment-wall-art/</link><pubDate>Thu, 01 Oct 2020 00:00:00 +0000</pubDate><author>hugh@kensiosoftware.co.uk (Hugh Grigg)</author><guid>https://kensiosoftware.co.uk/blog/etsy-integration-for-moment-wall-art/</guid><description>Moment Wall Art sells art pieces on its custom e-commerce website as well as via Etsy. I provided an automated Etsy integration to manage products, inventory…</description><content:encoded><![CDATA[<p><a href="https://momentwallart.co.uk/" title="Buy Art Prints">Moment Wall Art</a> sells art
pieces on its <a href="https://kensiosoftware.co.uk/freelance-e-commerce-development/">custom e-commerce website</a> as well as
via Etsy. I provided an automated <a href="https://kensiosoftware.co.uk/freelance-system-integration-development/">Etsy integration</a> to
manage products, inventory and orders between the two systems.</p>
<figure>
<img
src='/blog/etsy-integration-for-moment-wall-art/moment-wall-art-etsy-api-system-integration.jpg'
alt='Moment Wall Art Etsy Integration'
title='Moment Wall Art Etsy Integration'
>
</figure>
<p>The Etsy synchronisation system is implemented in Python. It reads and writes
between Moment Wall Art&rsquo;s bespoke e-commerce system and the Etsy API. Product
data including descriptions, images and pricing is kept up to date in Etsy,
while orders are synchronised back from Etsy into the e-commerce platform for
easier order processing and inventory management.</p>
<p>This integration runs in AWS Lambda as part of the
<a href="https://kensiosoftware.co.uk/freelance-serverless-systems/" title="Serverless Software Development">serverless architecture</a>
behind Moment Wall Art. Using serverless functions to run ad-hoc workloads
lowers costs and saves time that would otherwise need to be spent managing
servers and other infrastructure pieces.</p>
<p><a href="/contact">Contact me</a> about Etsy API integrations and development.</p>
]]></content:encoded><enclosure url="https://kensiosoftware.co.uk/blog/etsy-integration-for-moment-wall-art/moment-wall-art-etsy-api-system-integration.jpg" type="image/png"/><category>Python</category></item><item><title>Custom Wordpress development for Hacking Chinese</title><link>https://kensiosoftware.co.uk/blog/custom-wordpress-development-for-hacking-chinese/</link><pubDate>Wed, 02 Sep 2020 00:00:00 +0000</pubDate><author>hugh@kensiosoftware.co.uk (Hugh Grigg)</author><guid>https://kensiosoftware.co.uk/blog/custom-wordpress-development-for-hacking-chinese/</guid><description>Hacking Chinese is a Mandarin language education website. I provided bespoke WordPress development to better serve Hacking Chinese’s readers. The new…</description><content:encoded><![CDATA[<p><em>Hacking Chinese</em> is a Mandarin language education website. I provided bespoke
WordPress development to better serve <em>Hacking Chinese&rsquo;s</em> readers.</p>
<figure>
<img
src='/blog/custom-wordpress-development-for-hacking-chinese/wordpress-theme-development-hacking-chinese.jpg'
alt='Hacking Chinese Wordpress Theme Development'
title='Hacking Chinese Wordpress Theme Development'
>
</figure>
<p>The new WordPress theme is more efficient both on the backend when serving user
requests, and in its frontend CSS and JavaScript, compared to many off-the-shelf
options.</p>
<p>The new design is also mobile-first and responsive across a range of different
devices and network connectivity situations. This allows a greater number of
readers to benefit from Hacking Chinese&rsquo;s educational content.</p>
<p><a href="/contact">Contact me</a> about WordPress software development.</p>
]]></content:encoded><enclosure url="https://kensiosoftware.co.uk/blog/custom-wordpress-development-for-hacking-chinese/wordpress-theme-development-hacking-chinese.jpg" type="image/png"/></item><item><title>Etsy API and system integration for Pop Robin Cards</title><link>https://kensiosoftware.co.uk/blog/etsy-api-and-system-integration-for-pop-robin-cards/</link><pubDate>Tue, 01 Sep 2020 00:00:00 +0000</pubDate><author>hugh@kensiosoftware.co.uk (Hugh Grigg)</author><guid>https://kensiosoftware.co.uk/blog/etsy-api-and-system-integration-for-pop-robin-cards/</guid><description>As well as its bespoke e-commerce software, Pop Robin Cards also sells its 3D pop up cards via Etsy. This required a customised Etsy API integration, which I…</description><content:encoded><![CDATA[<p>As well as its bespoke e-commerce software,
<a href="https://www.poprobincards.co.uk/" title="3D Pop Up Cards">Pop Robin Cards</a>
also sells its 3D pop up cards via Etsy. This required a customised Etsy API
integration, which I was able to provide.</p>
<figure>
<img
src='/blog/etsy-api-and-system-integration-for-pop-robin-cards/pop-robin-cards-etsy-api-system-integration.jpg'
alt='Pop Robin Cards Etsy API Development'
title='Pop Robin Cards Etsy API Development'
>
</figure>
<p>The customised Etsy integration syncs product catalogue data from Pop Robin
Cards&rsquo; e-commerce system into the Etsy shop. This includes product names and
images, as well as Etsy-specific product descriptions that are generated from
product data using templates.</p>
<p>The integration is also able to make sure that other useful information is
included in the Etsy listing for each product, including shop notices and how
to add personalised gift messages when buying via Etsy.</p>
<p>Product inventory is also handled automatically, with stock levels being updated
from the main e-commerce system into Etsy, and also decremented in the main
system when orders are received in Etsy. This has reduced instances of
out-of-stock products and improved the customer experience for customers on both
platforms.</p>
<p>Orders are regularly synchronised back from Etsy into the central e-commerce
platform to provide a single order view for packing and dispatching orders. This
has improved efficiency and increased accuracy in order dispatch.</p>
<p>As all sales data is synced back into the main system, Pop Robin Cards is able
to benefit from aggregated statistics covering both sales platforms.</p>
<p><a href="/contact">Contact me</a> about development of software integrations.</p>
]]></content:encoded><enclosure url="https://kensiosoftware.co.uk/blog/etsy-api-and-system-integration-for-pop-robin-cards/pop-robin-cards-etsy-api-system-integration.jpg" type="image/png"/><category>Laravel</category></item><item><title>Custom e-commerce development for Moment Wall Art</title><link>https://kensiosoftware.co.uk/blog/custom-e-commerce-development-for-moment-wall-art/</link><pubDate>Sat, 01 Aug 2020 00:00:00 +0000</pubDate><author>hugh@kensiosoftware.co.uk (Hugh Grigg)</author><guid>https://kensiosoftware.co.uk/blog/custom-e-commerce-development-for-moment-wall-art/</guid><description>I developed a customised e-commerce system for Moment Wall Art. The e-commerce system is serverless to deliver a fast and efficient user experience at low…</description><content:encoded><![CDATA[<p>I developed a customised e-commerce system for
<a href="https://momentwallart.co.uk/" title="Wall Art Prints">Moment Wall Art</a>. The
e-commerce system is serverless to deliver a fast and efficient user experience
at low operational cost.</p>
<figure>
<img
src='/blog/custom-e-commerce-development-for-moment-wall-art/moment-wall-art-e-commerce-development.jpg'
alt='Moment Wall Art E-commerce Development'
title='Moment Wall Art E-commerce Development'
>
</figure>
<p>The e-commerce system manages Moment Wall Art&rsquo;s product catalogue, including a
detailed taxonomy system. This allows for specific catalogue pages such as
<a href="https://momentwallart.co.uk/japanese-wall-art-prints/">Japanese wall art</a>
and
<a href="https://momentwallart.co.uk/alphonse-mucha-wall-art-prints/">Alphonse Mucha Prints</a>,
letting customers find art pieces that interest them.</p>
<p>Despite being
<a href="https://kensiosoftware.co.uk/freelance-serverless-systems/">serverless</a>
with a static web frontend, the e-commerce website has a full product search
system. This allows searching for art prints using keywords such as
<a href="https://momentwallart.co.uk/search/?q=mucha+nouveau">&ldquo;Mucha nouveau&rdquo;</a> to find
specific art pieces that a customer might be interested in. The search solution
uses pre-generated JSON index files that are included in the static site build
and deployed to S3 for high-speed responses at low cost.</p>
<p>Stripe is integrated as the payment handler, providing a streamlined checkout
experience and secure transactions. This also allows the checkout process to
remain serverless, keeping operational costs low while maintaining high
availability.</p>
<p>The e-commerce system is also integrated with Etsy via their API, with inventory
synchronized out and orders synchronized back in to the main system.</p>
<p><a href="/contact">Contact me</a> about e-commerce software development.</p>
]]></content:encoded><enclosure url="https://kensiosoftware.co.uk/blog/custom-e-commerce-development-for-moment-wall-art/moment-wall-art-e-commerce-development.jpg" type="image/png"/></item><item><title>Custom web development for Chinese Boost</title><link>https://kensiosoftware.co.uk/blog/custom-web-development-for-chinese-boost/</link><pubDate>Wed, 01 Jul 2020 00:00:00 +0000</pubDate><author>hugh@kensiosoftware.co.uk (Hugh Grigg)</author><guid>https://kensiosoftware.co.uk/blog/custom-web-development-for-chinese-boost/</guid><description>I developed a custom education website for Chinese Boost. The website provides Mandarin Chinese learning materials and dynamic resources. The website is a…</description><content:encoded><![CDATA[<p>I developed a custom education website for
<a href="https://www.chineseboost.com/">Chinese Boost</a>. The website provides Mandarin
Chinese learning materials and dynamic resources.</p>
<figure>
<img
src='/blog/custom-web-development-for-chinese-boost/chinese-boost-custom-web-development.jpg'
alt='Chinese Boost Web Development'
title='Chinese Boost Web Development'
>
</figure>
<p>The website is a
<a href="https://kensiosoftware.co.uk/freelance-serverless-systems/">serverless solution</a>
with a static web frontend. This ensures high availability with low running
costs, as well as fast loading times for users.</p>
<p>The web development project includes a
<a href="https://www.chineseboost.com/chinese-example-sentences">Chinese example sentence search</a>
tool for users to find example usages of Chinese vocabulary that they are
studying. This is powered by a
<a href="https://kensiosoftware.co.uk/freelance-postgresql-developer/">Postgres SQL database</a>
and an
<a href="https://kensiosoftware.co.uk/freelance-elasticsearch-developer/">ElasticSearch</a>
fulltext search engine.</p>
<p>Another software tool that was implemented is a
<a href="https://www.chineseboost.com/tools/hanzi-pinyin-conversion">pinyin conversion tool</a>,
which allows users to generate pinyin romanisation from Chinese text. This is
implemented in PHP within the <a href="https://kensiosoftware.co.uk/freelance-laravel-developer/">Laravel framework</a>.</p>
<p><a href="/contact">Contact me</a> about web development.</p>
]]></content:encoded><enclosure url="https://kensiosoftware.co.uk/blog/custom-web-development-for-chinese-boost/chinese-boost-custom-web-development.jpg" type="image/png"/></item><item><title>Bespoke e-commerce development for Pop Robin Cards</title><link>https://kensiosoftware.co.uk/blog/bespoke-e-commerce-development-for-pop-robin-cards/</link><pubDate>Mon, 01 Jun 2020 00:00:00 +0000</pubDate><author>hugh@kensiosoftware.co.uk (Hugh Grigg)</author><guid>https://kensiosoftware.co.uk/blog/bespoke-e-commerce-development-for-pop-robin-cards/</guid><description>I developed a tailor-made, custom e-commerce system for Pop Robin Cards. Pop Robin Cards sells 3D pop up cards such as birthday cards and Christmas cards. The…</description><content:encoded><![CDATA[<p>I developed a tailor-made, custom e-commerce system for
<a href="https://www.poprobincards.co.uk/" title="3D Pop Up Cards">Pop Robin Cards</a>.</p>
<figure>
<img
src='/blog/bespoke-e-commerce-development-for-pop-robin-cards/pop-robin-cards-e-commerce-development.jpg'
alt='Pop Robin Cards E-commerce Development'
title='Pop Robin Cards E-commerce Development'
>
</figure>
<p>Pop Robin Cards sells 3D pop up cards such as
<a href="https://www.poprobincards.co.uk/3d-pop-up-birthday-cards" title="3D Pop Up Birthday Cards">birthday cards</a>
and
<a href="https://www.poprobincards.co.uk/3d-pop-up-christmas-cards" title="3D Pop Up Christmas Cards">Christmas cards</a>.
The bespoke e-commerce solution that I developed manages the
inventory and sales process end-to-end, including stock management, search,
payment and customer communications via e-mail.</p>
<p>The e-commerce product search system allows search for greetings cards, for
example those most relevant to
<a href="https://www.poprobincards.co.uk/catalogue/search?query=cherry+blossom" title="Cherry Blossom 3D Pop Up Cards">&ldquo;cherry blossom&rdquo;</a>.
This is powered by ElasticSearch with a Redis cache to keep it fast and
responsive.</p>
<p>Multiple payment systems including PayPal and Stripe are integrated to give
customers a choice of payment options. An Etsy API integration also syncs
products and orders with Etsy to generate more sales on an alternative
marketplace with centralised e-commerce management.</p>
<p>The web frontend is SEO optimised in several ways, including multiple taxonomy
systems for internal linking and discoverability. Products are linked from an
Occasion taxonomy, for example
<a href="https://www.poprobincards.co.uk/3d-pop-up-valentines-cards">Valentine&rsquo;s Cards</a>.
A Tag taxonomy indexes the themes and content of different product, such as
<a href="https://www.poprobincards.co.uk/pop-up-flower-cards">Flower Cards</a>. There is
also a Suitable Recipient taxonomy that allows viewing products in groupings
including
<a href="https://www.poprobincards.co.uk/pop-up-card-for-father">&ldquo;Cards for Father&rdquo;</a>.
These taxonomies are also combined to make more specific and helpful
categorisations such as
<a href="https://www.poprobincards.co.uk/pop-up-birthday-card-for-father">&ldquo;Birthday Cards for Father&rdquo;</a>.</p>
<p>This customised e-commerce platform also has several social media integrations.
Every page has meta tags optimised for social media, so that the best title,
description and social media image are selected when a page is shared on social
media or messaging platforms. The system also makes automated posts to social
networks based on trending products.</p>
<p>The order process includes a series of emails to inform the customer of the
status of their order, from order placement, dispatch and tracking codes from
Royal Mail.</p>
<p><a href="/contact">Contact me</a> about custom e-commerce development.</p>
]]></content:encoded><enclosure url="https://kensiosoftware.co.uk/blog/bespoke-e-commerce-development-for-pop-robin-cards/pop-robin-cards-e-commerce-development.jpg" type="image/png"/><category>AWS (Amazon Web Services)</category><category>ElasticSearch</category><category>Laravel</category><category>MySQL</category><category>Python</category></item><item><title>Wordpress conversion and web development for East Asia Student</title><link>https://kensiosoftware.co.uk/blog/east-asia-student-wordpress-conversion-web-development/</link><pubDate>Fri, 01 May 2020 00:00:00 +0000</pubDate><author>hugh@kensiosoftware.co.uk (Hugh Grigg)</author><guid>https://kensiosoftware.co.uk/blog/east-asia-student-wordpress-conversion-web-development/</guid><description>I provided conversion of a WordPress website into a fast and cost-efficient static website for East Asia Student. Due to a combination of high-quality content…</description><content:encoded><![CDATA[<p>I provided conversion of a WordPress website into a fast and
cost-efficient static website for
<a href="https://eastasiastudent.net/" title="East Asian Studies Website">East Asia Student</a>.</p>
<figure>
<img
src='/blog/east-asia-student-wordpress-conversion-web-development/east-asia-student-wordpress-conversion-development.jpg'
alt='East Asia Student Web Development'
title='East Asia Student Web Development'
>
</figure>
<p>Due to a combination of high-quality content and good SEO, the education website
receives a relatively high level of traffic, which was becoming expensive to
host for WordPress. Ensuring that the WordPress installation was up-to-date and
secure also required ongoing work. Converting the website from WordPress to a
static website system made it cheaper to operate, faster to use and more
secure.</p>
<p>A large part of the conversion work was extracting existing content from the
WordPress MySQL database and generating Markdown files to replace it. A Python
script automated the content conversion.</p>
<p>The WordPress theme was also replaced with a redesigned static theme with more
efficient CSS to render more quickly in users&rsquo; browsers.</p>
<p>The new static website is hosted in <a href="https://kensiosoftware.co.uk/freelance-aws-developer/">AWS</a> using <a href="/tech/s3">S3</a>,
<a href="/tech/cloudfront">Cloudfront</a> and <a href="/tech/route53">Route53</a>.</p>
<p><a href="/contact">Contact me</a> about WordPress conversion and AWS development.</p>
]]></content:encoded><enclosure url="https://kensiosoftware.co.uk/blog/east-asia-student-wordpress-conversion-web-development/east-asia-student-wordpress-conversion-development.jpg" type="image/png"/></item><item><title>General feature set for a fully-fledged application framework</title><link>https://kensiosoftware.co.uk/blog/general-feature-set-for-a-fully-fledged-application-framework/</link><pubDate>Sun, 15 Sep 2019 18:12:57 +0100</pubDate><author>hugh@kensiosoftware.co.uk (Hugh Grigg)</author><guid>https://kensiosoftware.co.uk/blog/general-feature-set-for-a-fully-fledged-application-framework/</guid><description>Entities Entity modelling, including entity relations. Aggregate Group of entities that can be interacted with as a single object. Commands Decoupled command…</description><content:encoded><![CDATA[<h2 id="entities">Entities</h2>
<p>Entity modelling, including entity relations.</p>
<h2 id="aggregate">Aggregate</h2>
<p>Group of entities that can be interacted with as a single object.</p>
<h2 id="commands">Commands</h2>
<p>Decoupled command objects that perform change operations on entities and
aggregates.</p>
<h2 id="queries">Queries</h2>
<p>Decoupled query objects that fetch single entities and collections of entities.</p>
<h2 id="validation">Validation</h2>
<ul>
<li>Command, query, entity and aggregate validation.</li>
<li>Static validation that can be defined at built-time.</li>
<li>Dynamic validation that can interact with live state at run-time.</li>
<li>Integrated with IAM so that different roles are permitted to perform
different commands and queries.</li>
</ul>
<h2 id="transitions--entity-lifecycles">Transitions / entity lifecycles</h2>
<p>Ability to move entities through stages in a lifecycle. For example, draft
products that can be published as products, or baskets that become orders via a
checkout process. This makes validation more flexible by allowing a gradual
assembly of a valid entity via intermediate stages.</p>
<h2 id="interfaces">Interfaces</h2>
<p>Decoupled interfaces that allow interaction with the application. Any command
or query can be issued via any supported interface.</p>
<h3 id="http">HTTP</h3>
<p>HTTP APIs, such as REST, OpenAPI, GraphQL.</p>
<h3 id="local-cli">Local CLI</h3>
<p>Heavy command line interface that runs application code locally to issue
commands and queries.</p>
<h3 id="network-cli">Network CLI</h3>
<p>Light command line interface that issues commands and queries over the network
to an API.</p>
<h3 id="web">Web</h3>
<p>Web user interface that issues commands and queries over the network to an API.
Could be purely HTML based, or use a web framework such as React or htmx.</p>
<h3 id="sdk">SDK</h3>
<p>Abstract interface in a programming language for use by other developers to
conveniently issue commands and queries over the network to an API.</p>
<h3 id="mobile--ios--android">Mobile / iOS / Android</h3>
<p>Mobile application user interfaces that issue commands and queries over the
network to an API.</p>
<h3 id="desktop">Desktop</h3>
<p>Desktop application user interface that issues commands and queries over the
network to an API.</p>
<h3 id="slack">Slack</h3>
<p>Slack chat integration built using the Slack SDK to allow issuing commands and
queries via intermediate event handlers on the application side.</p>
<h2 id="events">Events</h2>
<ul>
<li>Entity events (create, update, delete)</li>
<li>Configurable as synchronous or asynchronous via jobs.</li>
</ul>
<h2 id="tasks">Tasks</h2>
<ul>
<li>Asynchronous jobs that are handled on a queue.</li>
<li>Can bundle sub-tasks into a parent task and fan-out the handling.</li>
<li>Can mark a task as dependent on another so that the dependent tasks are
handled sequentially.</li>
<li>Incremental progress can be reported back to a Job entity, which can then be
pushed out via subscriptions or notifications.</li>
</ul>
<h2 id="authentication">Authentication</h2>
<ul>
<li>User and service authentication.</li>
<li>Third-party authentication via SAML.</li>
<li>External authentication via event handlers such as Slack event handlers, or
OS users for the local CLI.</li>
</ul>
<h2 id="authorization--permissions--iam">Authorization / Permissions / IAM</h2>
<ul>
<li>Static IAM that can be defined at build-time and hard-coded in the
application. This is fast.</li>
<li>Dynamic IAM that can be configured and applied at run-time, allowing it to
fetch live state. This is slow.</li>
</ul>
<h2 id="notifications">Notifications</h2>
<ul>
<li>Email</li>
<li>SMS</li>
<li>Outbound web-hooks</li>
<li>Web push</li>
<li>Mobile push</li>
<li>Slack</li>
<li>Telegram</li>
</ul>
<h2 id="inbound-web-hooks">Inbound web-hooks</h2>
<p>Convenience for setting up inbound web-hook endpoints for other systems to call
into and trigger events and audit trails.</p>
<h2 id="subscriptions">Subscriptions</h2>
<p>Allow clients to subscribe to updates via websocket.</p>
<h3 id="entity-subscription">Entity subscription</h3>
<p>Subscribe to changes to a specific individual entity or aggregate.</p>
<h3 id="series-subscription">Series subscription</h3>
<p>Receive incremental updates to a series, allowing chat applications and live
update feeds.</p>
<h2 id="scheduling">Scheduling</h2>
<ul>
<li>Cron-like scheduling of any command or task on a recurring basis.</li>
<li>One-off scheduling of a command or task at a specific time.</li>
</ul>
<h2 id="batch-processing">Batch processing</h2>
<ul>
<li>Handle changes across many entities by batching up work and handling it in
tasks.</li>
<li>Allow spreading batch work over intervals, e.g. &ldquo;hourly once a week&rdquo;. This
means that one batch is processed every hour, with all entities covered once
every week.</li>
</ul>
<h2 id="filtered-search">Filtered search</h2>
<ul>
<li>AKA faceted search or layered search.</li>
<li>Build a search index of an entity or aggregate.</li>
<li>Allow client to specify filters on configurable facets.</li>
<li>Results are paginated.</li>
<li>Filter metadata is returned with paginated results, showing the count of
entities matching each facet value.</li>
<li>Common in e-commerce applications.</li>
</ul>
<h2 id="infrastructure">Infrastructure</h2>
<ul>
<li>Framework assists with infrastructure management, based on configuration of
other resources such as entities, drivers and so on.</li>
<li>Docker</li>
<li>AWS CloudFormation</li>
<li>Terraform</li>
</ul>
<h2 id="drivers">Drivers</h2>
<ul>
<li>Resources as configurable providers so that the business logic is abstracted
from implementation.</li>
<li>E.g. entities and aggregates can be backed by a datastore driver, which can
be provided as different SQL databases, DynamoDB, filesystem, Redis etc.</li>
<li>Static drivers are configured at build-time, so the resulting application
only has that static driver available to it.</li>
<li>Dynamic drivers are configured at run-time, so that the same application
build can switch between drivers, for example in different environments.</li>
</ul>
<h2 id="deployments">Deployments</h2>
<ul>
<li>Support for deployment management by making deployments a first-class
concept and assisting with configuration of deployment tools such as Github
Actions, CodeBuild and so on.</li>
</ul>
<h2 id="logging">Logging</h2>
<ul>
<li>Default logging on entities, aggregates, commands, queries, events, tasks.</li>
<li>Driver-backed to allow configuring different log drivers.</li>
<li>Integrates with infrastructure to set up logging appropriate to configured
infrastructure.</li>
</ul>
<h2 id="monitoring-alerting">Monitoring, alerting</h2>
<ul>
<li>Driver-backed to allow configuring different log drivers.</li>
<li>Integrates with infrastructure to set up monitoring appropriate to
configured infrastructure.</li>
</ul>
<h2 id="migrations">Migrations</h2>
<ul>
<li>Datastore migrations.</li>
</ul>
<h2 id="auditing">Auditing</h2>
<ul>
<li>Support audit-trail logging to record commands, queries and tasks.</li>
</ul>
<h2 id="introspection">Introspection</h2>
<ul>
<li>Application can dynamically inspect itself at runtime and show infrastructure
data such as queue length.</li>
</ul>
<h2 id="tests">Tests</h2>
<p>Assistance with application test at different levels.</p>
<h3 id="unit">Unit</h3>
<p>Tests for self-contained individual units such as classes and functions that
don&rsquo;t have any dependencies outside of the constructor or function arguments.</p>
<h3 id="process">Process</h3>
<p>Tests that run in a single OS process, with any external dependency either
replaced with an in-process driver or mocked out with tools such as
<code>httpx-mock</code> and <code>moto</code>. These are highly effective for debugging business
logic, but will miss issues in the integration between real resources.</p>
<h3 id="integration">Integration</h3>
<p>Tests that run entirely on the local OS, with services like databases running in
Docker containers, and third-party APIs either mocked out with tools such as
<code>httpx-mock</code> or as dummy implementations running in Docker containers. The
application under test runs in the same process as the tests to allow debugging
during integration tests.</p>
<h3 id="deployed--smoke">Deployed / smoke</h3>
<p>Tests that run against the application in deployed infrastructure such as AWS.
These are few in number and aim for high-level sanity checks or smoke tests.</p>
<h2 id="environments">Environments</h2>
<p>Runtime environment awareness which lets the application behave differently in
different environments. Drivers can be specified per environment, for example
the local development environment might use a different queue driver than the
production deployment environment.</p>
<h2 id="local-development">Local development</h2>
<h2 id="feature-flags">Feature flags</h2>
<h2 id="configuration--settings">Configuration / settings</h2>
<ul>
<li>Static configuration is specified at build-time and supplied to the
application via environment variables or secure secrets. It cannot be changed
at run-time. It only needs to be loaded once at application start-up.</li>
<li>Dynamic configuration can be changed at run-time, either by administrators or
users. It needs to be loaded before each relevant action in case it has
changed.</li>
</ul>
<h2 id="demos">Demos</h2>
<p>Assistance for demoing features, for example a prompted command-line script that
takes the demo presenter through a particular use-case.</p>
<h2 id="documentation">Documentation</h2>
<p>Assistance for documenting the application, for example by producing OpenAPI
specifications.</p>
<h2 id="third-party-interfaces">Third-party interfaces</h2>
<p>Support for modelling third-party APIs, which can facilitate test
implementations and libraries.</p>
<h2 id="export">Export</h2>
<p>Ability to bulk export entities and aggregates in formats such as CSV and JSON.</p>
<h2 id="import">Import</h2>
<p>Ability to bulk import entities and aggregates from formats such as CSV and
JSON. Can use tasks that handle the import asynchronously and report granular
issues with individual rows.</p>
<h2 id="synchronisation">Synchronisation</h2>
<p>Two-way synchronisation with another system, so that updates flow in both
directions. For example, stock counts can be updated both to and from a
third-party sales system such as Amazon or Etsy.</p>
]]></content:encoded><enclosure url="https://kensiosoftware.co.uk/img/software-devices-640.png" type="image/png"/></item></channel></rss>