<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Nearform]]></title><description><![CDATA[Ideas, insights, and perspectives from inside the world of enterprise software development. Blog posts on design, software development, business and technology.]]></description><link>https://nearform.hashnode.dev</link><image><url>https://cdn.hashnode.com/res/hashnode/image/upload/v1706706218989/WbMGJE2vN.jpg</url><title>Nearform</title><link>https://nearform.hashnode.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Thu, 05 Nov 2026 10:02:02 GMT</lastBuildDate><atom:link href="https://nearform.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Announcement: Nearform will cease publishing content on Hashnode]]></title><description><![CDATA[We at Nearform have decided to stop publishing new blog posts on our Hashnode account. But don’t worry, we’ll still be publishing exclusive content from…]]></description><link>https://nearform.hashnode.dev/announcement-nearform-will-cease-publishing-content-on-hashnode</link><guid isPermaLink="true">https://nearform.hashnode.dev/announcement-nearform-will-cease-publishing-content-on-hashnode</guid><category><![CDATA[General Programming]]></category><category><![CDATA[iwritecode]]></category><category><![CDATA[Programming Blogs]]></category><dc:creator><![CDATA[Nearform]]></dc:creator><pubDate>Thu, 05 Dec 2024 09:23:56 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1733390905103/caa81970-2b0c-4735-aa92-8b1dec8d9108.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>We at Nearform have decided to stop publishing new blog posts on our Hashnode account. But don’t worry, we’ll still be publishing exclusive content from our tech experts. You can find our latest tech articles by visiting our <a target="_blank" href="https://www.nearform.com/digital-community/">Digital community</a>, at Nearform.com. </p>
<p>Our Hashnode account will remain live, so you can continue to read the articles we’ve previously published. We’re grateful to everyone who has read, liked and shared our content on Hashnode, and we hope you enjoy visiting our Digital community.</p>
]]></content:encoded></item><item><title><![CDATA[Save time and increase reliability with customisable blueprint unit test templates for Helm charts]]></title><description><![CDATA[By Iheanyi Onwubiko In our OPA policy-based testing of Helm charts blog post, we explored how to enforce standardised rules and best practices in…]]></description><link>https://nearform.hashnode.dev/save-time-and-increase-reliability-with-customisable-blueprint-unit-test-templates-for-helm-charts</link><guid isPermaLink="true">https://nearform.hashnode.dev/save-time-and-increase-reliability-with-customisable-blueprint-unit-test-templates-for-helm-charts</guid><category><![CDATA[Devops]]></category><category><![CDATA[Reliability]]></category><category><![CDATA[Kubernetes]]></category><category><![CDATA[helm chart]]></category><category><![CDATA[Testing]]></category><dc:creator><![CDATA[Nearform]]></dc:creator><pubDate>Fri, 15 Nov 2024 10:21:35 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1731592353782/f3013b76-73e0-41a3-a1fc-d6c6772a8634.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>By Iheanyi Onwubiko</p>
<h2 id="heading-devops-engineers-and-developers-discover-how-you-can-reduce-testing-time-and-improve-consistency-across-your-projects">DevOps engineers and developers, discover how you can reduce testing time and improve consistency across your projects</h2>
<p>In our <a target="_blank" href="https://www.nearform.com/digital-community/opa-policy-based-testing-of-helm-charts/">OPA policy-based testing of Helm charts</a> blog post, we explored how to enforce standardised rules and best practices in Kubernetes deployments. While a policy-based testing approach significantly enhances security and compliance in Kubernetes environments, this article shifts the spotlight to unit testing for Helm charts.</p>
<p>Unit testing complements policy-based testing by verifying the functional correctness of individual components within Helm charts. Unlike policy testing, which ensures adherence to predefined rules, unit testing validates that each part of a chart behaves as expected under various conditions.</p>
<p>This blog post introduces a solution designed to speed up testing and boost reliability using standardised, customisable blueprint templates that adapt to any chart.</p>
<h2 id="heading-leveraging-the-helm-unit-test-framework">Leveraging the Helm unit test framework</h2>
<p>We leveraged the <a target="_blank" href="https://github.com/helm-unittest/helm-unittest">Helm unit test framework</a> as the base of the test templates. This tool allows developers to write unit tests for Helm in YAML, a familiar format within the Kubernetes world. The framework simplifies testing by checking if the rendered YAML output from a chart matches the expected results. For example, a simple test to check if a deployment exists might look like this:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">suite:</span> <span class="hljs-string">test</span> <span class="hljs-string">deployment</span> <span class="hljs-string">existence</span>
<span class="hljs-attr">templates:</span>
  <span class="hljs-bullet">-</span> <span class="hljs-string">deployment.yaml</span>
<span class="hljs-attr">tests:</span>
  <span class="hljs-bullet">-</span> <span class="hljs-attr">it:</span> <span class="hljs-string">should</span> <span class="hljs-string">create</span> <span class="hljs-string">a</span> <span class="hljs-string">deployment</span>
    <span class="hljs-attr">asserts:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">hasKind:</span>
          <span class="hljs-attr">of:</span> <span class="hljs-string">Deployment</span>
</code></pre>
<p>This example test ensures that the chart generates a Kubernetes deployment resource. Let’s explore a more comprehensive example that showcases various testing techniques.</p>
<pre><code class="lang-yaml"><span class="hljs-attr">suite:</span> <span class="hljs-string">test</span> <span class="hljs-string">comprehensive</span> <span class="hljs-string">deployment</span> <span class="hljs-string">configuration</span>
<span class="hljs-attr">templates:</span>
  <span class="hljs-bullet">-</span> <span class="hljs-string">deployment.yaml</span>
<span class="hljs-attr">tests:</span>
  <span class="hljs-bullet">-</span> <span class="hljs-attr">it:</span> <span class="hljs-string">should</span> <span class="hljs-string">be</span> <span class="hljs-string">of</span> <span class="hljs-string">correct</span> <span class="hljs-string">kind,</span> <span class="hljs-string">apiVersion,</span> <span class="hljs-string">and</span> <span class="hljs-string">count</span>
    <span class="hljs-attr">asserts:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">hasDocuments:</span>
          <span class="hljs-attr">count:</span> <span class="hljs-number">1</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">isKind:</span>
          <span class="hljs-attr">of:</span> <span class="hljs-string">Deployment</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">isAPIVersion:</span>
          <span class="hljs-attr">of:</span> <span class="hljs-string">apps/v1</span>

    <span class="hljs-bullet">-</span> <span class="hljs-attr">it:</span> <span class="hljs-string">should</span> <span class="hljs-string">create</span> <span class="hljs-string">a</span> <span class="hljs-string">deployment</span> <span class="hljs-string">with</span> <span class="hljs-string">correct</span> <span class="hljs-string">specifications</span> <span class="hljs-string">and</span> <span class="hljs-string">metadata</span>
      <span class="hljs-attr">set:</span>
        <span class="hljs-attr">replicaCount:</span> <span class="hljs-number">3</span>
        <span class="hljs-attr">image:</span>
          <span class="hljs-attr">repository:</span> <span class="hljs-string">nginx</span>
          <span class="hljs-attr">tag:</span> <span class="hljs-number">1.19</span><span class="hljs-number">.0</span>
      <span class="hljs-attr">release:</span>
        <span class="hljs-attr">name:</span> <span class="hljs-string">production</span>
      <span class="hljs-attr">asserts:</span>
        <span class="hljs-bullet">-</span> <span class="hljs-attr">equal:</span>
            <span class="hljs-attr">path:</span> <span class="hljs-string">spec.replicas</span>
            <span class="hljs-attr">value:</span> <span class="hljs-number">3</span>
        <span class="hljs-bullet">-</span> <span class="hljs-attr">equal:</span>
            <span class="hljs-attr">path:</span> <span class="hljs-string">spec.template.spec.containers[0].image</span>
            <span class="hljs-attr">value:</span> <span class="hljs-string">nginx:1.19.0</span>
        <span class="hljs-bullet">-</span> <span class="hljs-attr">equal:</span>
            <span class="hljs-attr">path:</span> <span class="hljs-string">metadata.name</span>
            <span class="hljs-attr">value:</span> <span class="hljs-string">production-myapp</span>
        <span class="hljs-bullet">-</span> <span class="hljs-attr">matchRegex:</span>
            <span class="hljs-attr">path:</span> <span class="hljs-string">metadata.name</span>
            <span class="hljs-attr">pattern:</span> <span class="hljs-string">^production-.*$</span>
        <span class="hljs-bullet">-</span> <span class="hljs-attr">isNotEmpty:</span>
            <span class="hljs-attr">path:</span> <span class="hljs-string">spec.template.spec.containers[0].resources</span>
        <span class="hljs-bullet">-</span> <span class="hljs-attr">contains:</span>
            <span class="hljs-attr">path:</span> <span class="hljs-string">spec.template.spec.containers[0].ports</span>
            <span class="hljs-attr">content:</span>
              <span class="hljs-attr">containerPort:</span> <span class="hljs-number">80</span>
        <span class="hljs-bullet">-</span> <span class="hljs-attr">isNotNullOrEmpty:</span>
            <span class="hljs-attr">path:</span> <span class="hljs-string">metadata.labels</span>
        <span class="hljs-bullet">-</span> <span class="hljs-attr">equal:</span>
            <span class="hljs-attr">path:</span> <span class="hljs-string">metadata.labels.app</span>
            <span class="hljs-attr">value:</span> <span class="hljs-string">myapp</span>

  <span class="hljs-bullet">-</span> <span class="hljs-attr">it:</span> <span class="hljs-string">should</span> <span class="hljs-string">set</span> <span class="hljs-string">resource</span> <span class="hljs-string">limits</span>
    <span class="hljs-attr">asserts:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">equal:</span>
          <span class="hljs-attr">path:</span> <span class="hljs-string">spec.template.spec.containers[0].resources.limits.cpu</span>
          <span class="hljs-attr">value:</span> <span class="hljs-string">500m</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">equal:</span>
          <span class="hljs-attr">path:</span> <span class="hljs-string">spec.template.spec.containers[0].resources.limits.memory</span>
          <span class="hljs-attr">value:</span> <span class="hljs-string">512Mi</span>

  <span class="hljs-bullet">-</span> <span class="hljs-attr">it:</span> <span class="hljs-string">should</span> <span class="hljs-string">have</span> <span class="hljs-string">appropriate</span> <span class="hljs-string">security</span> <span class="hljs-string">context</span>
    <span class="hljs-attr">asserts:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">equal:</span>
          <span class="hljs-attr">path:</span> <span class="hljs-string">spec.template.spec.securityContext.runAsNonRoot</span>
          <span class="hljs-attr">value:</span> <span class="hljs-literal">true</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">isNull:</span>
          <span class="hljs-attr">path:</span> <span class="hljs-string">spec.template.spec.containers[0].securityContext.privileged</span>

  <span class="hljs-bullet">-</span> <span class="hljs-attr">it:</span> <span class="hljs-string">should</span> <span class="hljs-string">configure</span> <span class="hljs-string">liveness</span> <span class="hljs-string">probe</span> <span class="hljs-string">correctly</span>
    <span class="hljs-attr">asserts:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">isNotNull:</span>
          <span class="hljs-attr">path:</span> <span class="hljs-string">spec.template.spec.containers[0].livenessProbe</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">equal:</span>
          <span class="hljs-attr">path:</span> <span class="hljs-string">spec.template.spec.containers[0].livenessProbe.httpGet.path</span>
          <span class="hljs-attr">value:</span> <span class="hljs-string">/healthz</span>
</code></pre>
<p>This comprehensive test suite demonstrates a wide range of Helm unit testing capabilities:</p>
<p><strong>Basic structure validation:</strong></p>
<ul>
<li>Checks for the correct kind of action (deployment), API version (apps/v1) and document count.</li>
</ul>
<p><strong>Deployment specifications:</strong></p>
<ul>
<li><p>Uses “set” key to override chart values for testing specific scenarios.</p>
</li>
<li><p>Verifies replica count, image details and naming conventions.</p>
</li>
<li><p>Checks for the presence of resource specifications and correct port configuration.</p>
</li>
</ul>
<p><strong>Metadata and labelling:</strong></p>
<ul>
<li><p>Ensures the correct naming convention (combining release name and app name) is used.</p>
</li>
<li><p>Verifies the presence and content of labels.</p>
</li>
</ul>
<p><strong>Resource management:</strong></p>
<ul>
<li>Checks for specific CPU and memory limits, crucial for resource allocation in Kubernetes.</li>
</ul>
<p><strong>Security configurations:</strong></p>
<ul>
<li>Validates security context settings, ensuring the container runs as a non-root user and isn't privileged.</li>
</ul>
<p><strong>Health checks:</strong></p>
<ul>
<li>Verifies the presence and configuration of a liveness probe.</li>
</ul>
<p>You can find more information on how to define tests <a target="_blank" href="https://github.com/helm-unittest/helm-unittest/blob/main/DOCUMENT.md">here</a>.</p>
<p>While this test suite is suitable for a specific deployment, it is tightly coupled to the “myapp” application and the “production” release. How can we efficiently reuse this test or a set of test suites across various charts without duplicating effort? To make this more versatile, we could introduce placeholders like {{ CHART_NAME }}, {{ RELEASE_NAME }}, {{ CPU_LIMIT }}, {{ HEALTH_CHECK_PATH }}, {{ REPLICA_COUNT }} etc., to make this specific test suite into a reusable blueprint that can be easily adapted to different charts and configurations.</p>
<h2 id="heading-blueprint-unit-test-templates">Blueprint unit test templates</h2>
<p>A “templated” Helm test suite allows a test to be reused with different charts, aligning with the best practices of modularity and reusability. In other words, we consume blueprint, standardised, pre-written test suites that can be used to suit any project’s needs. For instance, if we have a test that needs to verify the chart name, release name, deployment count and common metadata, the template includes the checks and can be adjusted per project requirements.</p>
<pre><code class="lang-yaml"><span class="hljs-attr">suite:</span> <span class="hljs-string">test</span> {{ <span class="hljs-string">CHART_NAME</span> }} <span class="hljs-string">deployment</span>
<span class="hljs-attr">templates:</span>
  <span class="hljs-bullet">-</span> <span class="hljs-string">deployment.yaml</span>
<span class="hljs-attr">tests:</span>
  <span class="hljs-bullet">-</span> <span class="hljs-attr">it:</span> <span class="hljs-string">should</span> <span class="hljs-string">be</span> <span class="hljs-string">of</span> <span class="hljs-string">correct</span> <span class="hljs-string">kind,</span> <span class="hljs-string">apiVersion,</span> <span class="hljs-string">and</span> <span class="hljs-string">count</span>
    <span class="hljs-attr">asserts:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">hasDocuments:</span>
          <span class="hljs-attr">count:</span> {{ <span class="hljs-string">DEPLOYMENT_COUNT</span> }}
      <span class="hljs-bullet">-</span> <span class="hljs-attr">isKind:</span>
          <span class="hljs-attr">of:</span> <span class="hljs-string">Deployment</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">isAPIVersion:</span>
          <span class="hljs-attr">of:</span> <span class="hljs-string">apps/v1</span>
  <span class="hljs-bullet">-</span> <span class="hljs-attr">it:</span> <span class="hljs-string">should</span> <span class="hljs-string">have</span> <span class="hljs-string">common</span> <span class="hljs-string">metadata</span>
    <span class="hljs-attr">release:</span>
      <span class="hljs-attr">name:</span> {{ <span class="hljs-string">RELEASE_NAME</span> }}
    <span class="hljs-attr">asserts:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">equal:</span>
          <span class="hljs-attr">path:</span> <span class="hljs-string">metadata.name</span>
          <span class="hljs-attr">value:</span> {{ <span class="hljs-string">RELEASE_NAME</span> }}<span class="hljs-string">-{{</span> <span class="hljs-string">CHART_NAME</span> <span class="hljs-string">}}</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">isNotNullOrEmpty:</span>
          <span class="hljs-attr">path:</span> <span class="hljs-string">metadata.labels</span>
  <span class="hljs-bullet">-</span> <span class="hljs-attr">it:</span> <span class="hljs-string">should</span> <span class="hljs-string">set</span> <span class="hljs-string">resource</span> <span class="hljs-string">limits</span>
    <span class="hljs-attr">asserts:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">equal:</span>
          <span class="hljs-attr">path:</span> <span class="hljs-string">spec.template.spec.containers[0].resources.limits.cpu</span>
          <span class="hljs-attr">value:</span> {{ <span class="hljs-string">CPU_LIMIT</span> }}
      <span class="hljs-bullet">-</span> <span class="hljs-attr">equal:</span>
          <span class="hljs-attr">path:</span> <span class="hljs-string">spec.template.spec.containers[0].resources.limits.memory</span>
          <span class="hljs-attr">value:</span> {{ <span class="hljs-string">MEMORY_LIMIT</span> }}
</code></pre>
<p>The above test suite can now serve as a versatile blueprint unit test template, which DevOps practitioners can then fine-tune based on their chart’s specific set-up. This approach enables uniform and streamlined testing across diverse Helm charts, guaranteeing that each deployment fulfils the necessary criteria while reducing the labour required to develop and upkeep test suites.</p>
<p>To further streamline this process and fully leverage the benefits of this method, we wrote a custom Python script that converts the blueprints into customised test suites for each unique chart.</p>
<h3 id="heading-processing-the-blueprints"><strong>Processing the blueprints</strong></h3>
<p>To automate the creation of the final unit tests, we use a Python script that processes the blueprint test suites. This script replaces placeholders in the test suites with actual values from a configuration file. This step transforms our generic blueprints into chart-specific test suites. To demonstrate how to use this solution, let’s walk through a practical scenario using the blueprint deployment and ConfigMap test suites. Reference the sample code in the public repository <a target="_blank" href="https://github.com/nearform/helm-unit-test-templates">here</a> to follow along.</p>
<h3 id="heading-preparing-the-environment"><strong>Preparing the environment</strong></h3>
<p>Before we begin, we need to have the following prerequisites installed:</p>
<ul>
<li><p><a target="_blank" href="https://helm.sh/docs/intro/install/">Helm</a> &gt; v3.x</p>
</li>
<li><p>Python &gt; 3.10</p>
</li>
<li><p>Git</p>
</li>
<li><p>Local Kubernetes cluster: Although not strictly required for testing purposes, setting up a local Kubernetes cluster with tools like <a target="_blank" href="https://minikube.sigs.k8s.io/docs/start/">minikube</a> is advisable. This enables validation of the Helm charts in a Kubernetes environment.</p>
</li>
</ul>
<p>You can verify installations by running:</p>
<pre><code class="lang-bash">helm version
Python --version
git --version
</code></pre>
<p>Clone the repository:</p>
<pre><code class="lang-bash">git <span class="hljs-built_in">clone</span> https://github.com/nearform/helm-unit-test-templates/
<span class="hljs-built_in">cd</span> helm-unit-test-templates/sample-code
</code></pre>
<p>This is the folder structure:</p>
<pre><code class="lang-bash">sample-code/
├── README.md
├── mars/
│   ├── Chart.yaml
│   ├── templates/
│   ├── tests/
│   │   ├── deployment_test.yml
│   │   ├── configmap_test.yml
│   │   └── ...
│   └── values.yaml
├── notes_test.yml
├── renderer.py
├── run_tests.sh
└── values.json
</code></pre>
<p>Mars is the Helm chart we will use for the demo. The test suite templates are located in the mars/tests/ folder. We’ll focus on <code>deployment_test.yml</code> and <code>configmap_test.yml</code> to illustrate how these test suite templates work. Let’s take a look at the <code>configmap_test.yml</code> file:</p>
<pre><code class="lang-yaml"><span class="hljs-comment"># yaml-language-server: $schema=https://raw.githubusercontent.com/helm-unittest/helm-unittest/main/schema/helm-testsuite.json</span>
<span class="hljs-attr">suite:</span> <span class="hljs-string">test</span> {{ <span class="hljs-string">CHART_NAME</span> }} <span class="hljs-string">configmap</span>
<span class="hljs-attr">templates:</span>
  <span class="hljs-bullet">-</span> <span class="hljs-string">configmap.yaml</span>
<span class="hljs-attr">tests:</span>
  <span class="hljs-bullet">-</span> <span class="hljs-attr">it:</span> <span class="hljs-string">should</span> <span class="hljs-string">be</span> <span class="hljs-string">of</span> <span class="hljs-string">correct</span> <span class="hljs-string">kind,</span> <span class="hljs-string">apiVersion,</span> <span class="hljs-string">and</span> <span class="hljs-string">count</span>
    <span class="hljs-attr">asserts:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">hasDocuments:</span>
          <span class="hljs-attr">count:</span> {{ <span class="hljs-string">CONFIGMAP_COUNT</span> <span class="hljs-string">|</span> <span class="hljs-string">default(1)</span> }}
      <span class="hljs-bullet">-</span> <span class="hljs-attr">isKind:</span>
          <span class="hljs-attr">of:</span> <span class="hljs-string">ConfigMap</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">isAPIVersion:</span>
          <span class="hljs-attr">of:</span> <span class="hljs-string">v1</span>

  <span class="hljs-bullet">-</span> <span class="hljs-attr">it:</span> <span class="hljs-string">should</span> <span class="hljs-string">have</span> <span class="hljs-string">common</span> <span class="hljs-string">labels</span>
    <span class="hljs-attr">asserts:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">isNotNullOrEmpty:</span>
          <span class="hljs-attr">path:</span> <span class="hljs-string">metadata.labels</span>

  <span class="hljs-bullet">-</span> <span class="hljs-attr">it:</span> <span class="hljs-string">should</span> <span class="hljs-string">have</span> <span class="hljs-string">correct</span> <span class="hljs-string">name</span> <span class="hljs-string">(standard)</span>
    <span class="hljs-attr">documentIndex:</span> <span class="hljs-number">0</span>
    <span class="hljs-attr">release:</span>
      <span class="hljs-attr">name:</span> {{ <span class="hljs-string">RELEASE_NAME</span> }}
    <span class="hljs-attr">asserts:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">equal:</span>
          <span class="hljs-attr">path:</span> <span class="hljs-string">metadata.name</span>
          <span class="hljs-attr">value:</span> {{ <span class="hljs-string">RELEASE_NAME</span> }}<span class="hljs-string">-{{</span> <span class="hljs-string">CHART_NAME</span> <span class="hljs-string">}}</span>

  <span class="hljs-bullet">-</span> <span class="hljs-attr">it:</span> <span class="hljs-string">should</span> <span class="hljs-string">not</span> <span class="hljs-string">have</span> <span class="hljs-string">Helm</span> <span class="hljs-string">hook</span> <span class="hljs-string">(standard)</span>
    <span class="hljs-attr">documentIndex:</span> <span class="hljs-number">0</span>
    <span class="hljs-attr">asserts:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">notExists:</span>
          <span class="hljs-attr">path:</span> <span class="hljs-string">metadata.annotations</span>

  <span class="hljs-bullet">-</span> <span class="hljs-attr">it:</span> <span class="hljs-string">should</span> <span class="hljs-string">contain</span> <span class="hljs-string">expected</span> <span class="hljs-string">environment</span> <span class="hljs-string">variables</span>
    <span class="hljs-attr">asserts:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">equal:</span>
          <span class="hljs-attr">path:</span> <span class="hljs-string">data</span>
          <span class="hljs-attr">value:</span>
            {{ <span class="hljs-string">CONFIGMAP_DATA</span> <span class="hljs-string">|</span> <span class="hljs-string">to_yaml</span> <span class="hljs-string">|</span> <span class="hljs-string">indent(12)</span> }}
</code></pre>
<p>To customise the tests for a specific chart, we’ll need to modify the <code>sample-code/values.json</code> file. This file provides the actual values for the placeholders in the test templates. Let’s update this file with appropriate values for our Helm chart. You’ll notice that placeholders will be replaced when the tests are rendered.</p>
<pre><code class="lang-json">{
    <span class="hljs-attr">"CHART_NAME"</span>: <span class="hljs-string">"mars"</span>,
    <span class="hljs-attr">"RELEASE_NAME"</span>: <span class="hljs-string">"foobar"</span>,
    <span class="hljs-attr">"CONFIGMAP_COUNT"</span>: <span class="hljs-number">1</span>,
    <span class="hljs-attr">"DEPLOYMENT_COUNT"</span>: <span class="hljs-number">2</span>,
    <span class="hljs-attr">"CONFIGMAP_DATA"</span>: {
      <span class="hljs-attr">"TZ"</span>: <span class="hljs-string">"UTC"</span>,
      <span class="hljs-attr">"JOHN"</span>: <span class="hljs-string">"DOE"</span>
    }
  }
</code></pre>
<p>With the values.json file configured, we’re ready to initiate the testing process. To do this, we’ll invoke the run_tests.sh script, which orchestrates the entire testing workflow as follows:</p>
<pre><code class="lang-bash">./run_tests.sh path/to/chart values.json
</code></pre>
<p>This command invokes the script with two arguments: the path to the chart we are testing (sample-code/mars) and the <code>values.json</code> file we just modified i.e <code>./run_tests.sh mars values.json</code>.</p>
<p>Behind the scenes, the script sets in motion a series of operations:</p>
<ol>
<li><p>It calls <code>renderer.py</code>, a Python script that acts as the engine for our testing framework.</p>
</li>
<li><p>The Python script performs several key tasks:</p>
<p> a. It generates a temporary directory named &lt;chart-name&gt;_rendered, which serves as a “sandbox” for our testing.</p>
<p> b. The script then duplicates the contents of the original chart into this new directory.</p>
<p> c. Next, it scans the test folder within mars_rendered for the templated YAML test suites</p>
<p> d. For each discovered test file, <code>renderer.py</code> it applies the Jinja2 templating engine, substituting the placeholders we discussed earlier with actual values from the values.json file.</p>
<p> e. The resulting fully formed test files are then saved back into the mars_rendered/tests directory.</p>
</li>
<li><p>With the tests now prepared, <code>run-tests.sh</code> invokes the helm unit test command directing it to evaluate the chart in the “sandbox” directory.</p>
</li>
<li><p>As the tests execute, you’ll see real-time feedback in the terminal, highlighting any successes or failures.</p>
</li>
<li><p>Upon completion of the tests, the script makes a decision:</p>
<p> a. If all tests pass successfully, it tidies things up by removing the temporary mars_rendered directory.</p>
<p> b. However, if any tests fail, the script preserves the mars_rendered directory, allowing for inspection of the generated files and diagnosis of any issues.</p>
</li>
</ol>
<p><img src="https://www.nearform.com/media/pages/digital-community/save-time-and-increase-reliability-with-customisable-blueprint-unit-test-templates-for-helm-charts/62be4bef3a-1731404947/blog-save-time-and-increase-reliability-with-customisable-blueprint-unit-test-templates-for-helm-charts-test-result-1600x.png" alt="Test result showing failed and passed tests" /></p>
<p>Fig 1: Test result showing failed and passed tests</p>
<p>The above snapshot shows the results of the tests. Overall, out of 47 total tests across five test suites, 45 passed and two failed, with the entire process being completed in about 146 milliseconds. Scrutinising the test suites in the mars_rendered directory allows you to compare the rendered test expectations against the actual chart output, helping pinpoint the exact source of the discrepancy.</p>
<p>To resolve the ConfigMap test failure, trace the issue by examining the rendered ConfigMap in <code>mars_rendered/templates/configmap.yaml</code>. Compare this with the test expectations in <code>marsrendered/tests/configmaptest.yaml</code>. This investigation will lead you to the env section in the chart's <code>values.yaml</code> file, where you'll need to add the missing JOHN: "DOE" entry under the “plain” subsection to match the test's expectations. This adjustment will ensure the ConfigMap test passes by providing all expected environment variables.</p>
<p><img src="https://www.nearform.com/media/pages/digital-community/save-time-and-increase-reliability-with-customisable-blueprint-unit-test-templates-for-helm-charts/a84666fa85-1731405080/blog-save-time-and-increase-reliability-with-customisable-blueprint-unit-test-templates-for-helm-charts-configmap-data-1600x.png" alt="Resolving the ConfigMap data" /></p>
<p>Fig 2: Resolving the ConfigMap data</p>
<p>The deployment test failed — it expected two deployment documents but found only one. This could indicate an issue with the chart's deployment configuration or a mismatch between the expected and actual number of deployments. To address it, we'll modify the <code>values.json</code> file to set DEPLOYMENT_COUNT to 1, aligning with the actual number of deployments in our chart. This change will resolve the deployment test failure.</p>
<p><img src="https://www.nearform.com/media/pages/digital-community/save-time-and-increase-reliability-with-customisable-blueprint-unit-test-templates-for-helm-charts/0f5c34f982-1731405196/blog-save-time-and-increase-reliability-with-customisable-blueprint-unit-test-templates-for-helm-charts-deployment-count-1600x.png" alt="Resolving the deployment count" /></p>
<p>Fig 3: Resolving the deployment count</p>
<p>After making these changes, we re-run the test script (./run_tests.sh mars values.json) to verify the fixes. By aligning the test expectations with your chart's configuration, both the deployment and ConfigMap tests should pass successfully.</p>
<p><img src="https://www.nearform.com/media/pages/digital-community/save-time-and-increase-reliability-with-customisable-blueprint-unit-test-templates-for-helm-charts/e0817eae1b-1731405275/blog-save-time-and-increase-reliability-with-customisable-blueprint-unit-test-templates-for-helm-charts-successful-test-1600x.png" alt="A successful test" /></p>
<p>Fig 4: A successful test</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>DevOps teams can attain substantial efficiency gains and enhanced reliability by embracing these blueprint Helm unit test templates. Complementing policy-based testing, they validate that Helm charts generate Kubernetes resources with expected structures and properties, verifying compliance from a security and organisation perspective. The templates are easy to adapt for specific chart requirements, significantly reduce testing time through reusable templates, ensure consistency with standardised yet customisable structures and improve deployment reliability.</p>
]]></content:encoded></item><item><title><![CDATA[4 pillars for crafting a design system for existing products]]></title><description><![CDATA[By Nick Zafiropoulos When considering that two-thirds of companies report using them, it’s clear design systems hold a significant place in tech product…]]></description><link>https://nearform.hashnode.dev/4-pillars-for-crafting-a-design-system-for-existing-products</link><guid isPermaLink="true">https://nearform.hashnode.dev/4-pillars-for-crafting-a-design-system-for-existing-products</guid><category><![CDATA[Design Systems]]></category><category><![CDATA[scalability]]></category><category><![CDATA[Product Design]]></category><category><![CDATA[#product-roadmap]]></category><category><![CDATA[Design]]></category><dc:creator><![CDATA[Nearform]]></dc:creator><pubDate>Thu, 07 Nov 2024 17:10:41 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1730988642798/a581a2d1-a505-4b86-a110-0ea2b6c6a0ba.webp" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>By Nick Zafiropoulos</p>
<h2 id="heading-discover-how-to-lay-a-foundation-that-ensures-your-design-system-is-aligned-with-an-organisations-short-mid-and-long-term-goals">Discover how to lay a foundation that ensures your design system is aligned with an organisation’s short, mid and long-term goals</h2>
<p>When considering that <a target="_blank" href="https://www.salesforce.com/blog/most-companies-use-design-systems-do-you/#:~:text=Two%2Dthirds%20of%20companies%20report%20using%20a%20design%20system.\)">two-thirds of companies</a> report using them, it’s clear design systems hold a significant place in tech product ecosystems. They ensure consistency, encourage cohesiveness and help maintain a business’ vision across its products and services.</p>
<p>As with the scope of any project, there will always be variables that require adapting methods. However, crafting a design system for an existing product/organisation can be achieved by focusing collaborative discussions and output around four key pillars:</p>
<ul>
<li><p>Identify, plan and prioritise</p>
</li>
<li><p>Assess and evaluate</p>
</li>
<li><p>Communicate, collaborate and build</p>
</li>
<li><p>Document and plan</p>
</li>
</ul>
<p>Adopting these pillars, fosters clarity, encourages buy-in across teams and enables gradual, thoughtful scaling. Now we’ll explore each of them in detail.</p>
<h2 id="heading-pillar-1-identify-plan-and-prioritise">Pillar #1: Identify, plan and prioritise</h2>
<p>Work with key stakeholders around the organisation who are involved with the product(s) to identify and clarify problems, pain points and opportunities.  Once you’ve done this, decide how to prioritise the work. Some questions that should be asked are:</p>
<ul>
<li><p>What are the common sources of friction? Are there inconsistencies, or technical misinterpretations? Is there an underlying misalignment of values being translated into the product(s)?</p>
</li>
<li><p>What opportunities present themselves to address the identified problems? Can we fix <em>x</em> by focusing on <em>y?</em></p>
</li>
<li><p>How do we ensure we build solid foundations for the design system that allows it to scale and evolve?</p>
</li>
<li><p>What should we focus on first? What are the basics that can start to deliver real impact quickly and that can start to create ripples of excitement around other parts of the organisation?</p>
</li>
</ul>
<p>Not all of these considerations need to translate into direct actions for early work on a design system, but they can be useful to be aware of as the system grows and evolves in the future.</p>
<p>As part of <a target="_blank" href="https://www.nearform.com/work/telus/">Nearform’s work with TELUS Digital</a>, the web arm of Canadian telecoms giant TELUS, we facilitated early discovery sessions. These took a deep dive into the root causes and problems that were resulting in fragmented experiences amongst their various products and services.</p>
<p>Through identifying, planning and prioritising, we were able to recommend and build a universal design system, ensuring a seamless experience across all channels. The result was a more robust system that allowed teams around the organisation to design, build and release products — with better UI and UX — much more efficiently than before.</p>
<p>Our partner saw an incredible impact, as Steve Tannock, TELUS Digital’s Director of Platform, Technology &amp; Tools, notes: “Generally the time to market for any new app is about 30-40% less than it was, which is huge!”</p>
<h2 id="heading-pillar-2-assess-and-evaluate">Pillar #2: Assess and evaluate</h2>
<p>Assess and evaluate the current state of the product(s) and the organisation and how they fit together. Focusing on the following can help to build a holistic view of the current situation:</p>
<ul>
<li><p>Conduct a review of the product(s), examining existing core UI components, accessibility standards and common patterns and behaviours across the product(s).</p>
</li>
<li><p>Collect the existing styles, components and flows, to assess what needs to go into the core of the design system, and decide how you can then structure the output.</p>
</li>
<li><p>Optionally, not always required in the first phase, collect other, less tech-focused resources that may complement and inform the product's design. For example, brand guidelines, tone of voice and company values.</p>
</li>
</ul>
<p>You can explore a practical example of the assess and evaluate pillar by reading PUMA’s story of <a target="_blank" href="https://commerce.nearform.com/work/puma-design-system">building a global unified design system</a>.</p>
<h2 id="heading-pillar-3-communicate-collaborate-and-build">Pillar #3: Communicate, collaborate and build</h2>
<p>Regular communication with the business is important, to ensure you’re staying on track to deliver a valuable and scalable system. Here are some ways in which a collaborative approach can help build a better design system:</p>
<ul>
<li><p>UI Components in Figma (or another design tool) should align with existing frontend components. Naming conventions and structures can be aligned to create more cohesive workflows between designers and engineers.</p>
</li>
<li><p>Validate your work regularly. Staying on track is important. design systems can cover many angles, and it can be easy to veer off course, focusing on output that may not be the immediate priority. Demoing progress and communicating next steps will help to build excitement early, fostering greater advocacy and easier adoption and growth into the future.</p>
</li>
</ul>
<h2 id="heading-pillar-4-document-and-plan">Pillar #4: Document and plan</h2>
<p>A design system should be usable and understandable by people and teams all around the organisation. Components should be clearly explained, styles should be easily applied and values well communicated.</p>
<p>There are many ways to document a design system — encapsulating everything from simple brand messaging, to code snippets and accessibility guidelines. With documentation, it’s best to start small, and then listen to the consumers (designers, engineers etc) of the design system to see what could add value over time.</p>
<p>Deciding on a documentation platform and structure is a good start. Documentation could initially live within the design tool (Figma, for example). But over time, tools like Storybook, Docusaurus and zeroheight could be considered, as they may provide a better home that is easier to manage and more useful to engineering teams. Building incrementally as the design system grows and matures ensures an even focus is maintained.</p>
<p>In addition to this, to ensure the design system is set up for ongoing success in the future, contribution models and governance planning should be realistically considered to align with team size, capabilities and product roadmaps.</p>
<h2 id="heading-conclusion-build-a-solid-foundation-then-reflect-understand-plan-and-prioritise-future-versions">Conclusion: Build a solid foundation then reflect, understand, plan and prioritise future versions</h2>
<p>Building a design system for an existing product can deliver real value, bringing greater consistency and awareness within an organisation, whilst setting forth a solid path for new features, capabilities and scaling opportunities.</p>
<p>Although it’s important to understand the goals of each organisation, it’s often best to start small and scale over time depending on needs. However small the first phase of work is, ensuring you work within the four pillars will help to cement a solid base for evolution over time.</p>
<p>Build the first version of the system to solve one or two problems (for example, UI consistency standards for designers), and then look to the future to see how it can be widened out to benefit other areas of the organisation (for example, alignment of brand and guiding principles).</p>
<p>Once you’ve built a solid base, take some time to reflect, understand, plan and prioritise future versions that can elevate the design system from a useful artefact to a comprehensive resource that is a central hub for the whole organisation.</p>
]]></content:encoded></item><item><title><![CDATA[A 5-stage guide to turbocharging the quality assurance process with generative AI]]></title><description><![CDATA[By Aska Cloke There are many blogs and discussions about utilising generative artificial intelligence (GenAI) for quality assurance (QA) activities, which…]]></description><link>https://nearform.hashnode.dev/a-5-stage-guide-to-turbocharging-the-quality-assurance-process-with-generative-ai</link><guid isPermaLink="true">https://nearform.hashnode.dev/a-5-stage-guide-to-turbocharging-the-quality-assurance-process-with-generative-ai</guid><category><![CDATA[General Programming]]></category><category><![CDATA[iwritecode]]></category><category><![CDATA[Programming Blogs]]></category><category><![CDATA[Quality Assurance]]></category><category><![CDATA[generative ai]]></category><dc:creator><![CDATA[Nearform]]></dc:creator><pubDate>Thu, 17 Oct 2024 09:57:49 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1728996422401/39c6b4eb-19bf-459a-a3a2-e047ab20771f.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>By Aska Cloke</p>
<h2 id="heading-learn-how-to-enhance-test-productivity-and-quality-at-each-stage-of-the-software-development-life-cycle-sdlc">Learn how to enhance test productivity and quality at each stage of the Software Development Life Cycle (SDLC)</h2>
<p>There are many blogs and discussions about utilising generative artificial intelligence (GenAI) for quality assurance (QA) activities, which primarily focuses on test cases and automation script generation. In this blog, I explore how we can enhance test productivity and quality at each stage of the Software Development Life Cycle (SDLC) by leveraging GenAI.</p>
<p>By incorporating AI into this process from a QA point of view, I believe the practice of 'Shift Left' (this moves testing activities closer to the beginning of the SDLC) will occur organically. To prove this point, I used <a target="_blank" href="https://chatgpt.com/">ChatGPT</a> and <a target="_blank" href="https://www.continue.dev/">Continue</a> (VSCode extension) to create examples. This article may be useful for anyone involved in software development, but the focus is predominantly on testing activities.</p>
<h2 id="heading-software-development-life-cycle">Software Development Life Cycle</h2>
<p>There are five main stages in the SDLC:</p>
<ul>
<li><p>Analysis</p>
</li>
<li><p>Design</p>
</li>
<li><p>Implementation</p>
</li>
<li><p>Testing</p>
</li>
<li><p>Evolution/maintenance</p>
</li>
</ul>
<p><img src="https://www.nearform.com/media/pages/digital-community/a-5-stage-guide-to-turbocharging-the-quality-assurance-process-with-generative-ai/64ab68c847-1728486262/blog-a-5-stage-guide-to-turbocharging-the-quality-assurance-process-with-generative-ai-software-dev-life-cycle-img-1600x.jpg" alt="Software development life cycle" /></p>
<p>Source: <a target="_blank" href="https://commons.wikimedia.org/wiki/File:SDLC_-_Software_Development_Life_Cycle.jpg">Software Development Life Cycle</a></p>
<p>I will use the <a target="_blank" href="https://www.ag-grid.com/example-hr/">AG Grid’s HR Demo</a> site as a sample application throughout this process. The example task is to add a new functionality to this application as described in the story below:</p>
<p><em>As a HR system user</em></p>
<p><em>I want to add a column for Employee’s nationality next to the Country of Residence</em></p>
<p><em>So that I can identify whether employee requires a work permit</em></p>
<p>Let’s follow each stage of the SDLC using this story to identify how we can enhance QA activities with GenAI.</p>
<h2 id="heading-1-analysis">1. Analysis</h2>
<p>In the analysis stage, information is gathered from stakeholders regarding their requirements and expectations of the system.</p>
<ul>
<li><p>User stories are created at this stage.</p>
</li>
<li><p>A ‘Three Amigos’ session (a Business Analyst, a Developer and a Quality Assurance Tester get together to review requirements) may take place after identifying the requirements.</p>
</li>
<li><p>Before or during the ‘Three Amigos’ session, we can use ChatGPT to generate test cases for the user stories to explore how much value it adds.</p>
</li>
</ul>
<p>This is what I asked in ChatGPT:</p>
<p><img src="https://www.nearform.com/media/pages/digital-community/a-5-stage-guide-to-turbocharging-the-quality-assurance-process-with-generative-ai/14f1b5eee0-1728476483/blog-a-5-stage-guide-to-turbocharging-the-quality-assurance-process-with-generative-ai-chat-gpt-1600x.png" alt="ChatGPT command" /></p>
<p>It generated seven test cases. I do not want to bore you with listing all test cases, so I selected a few below that could lead to a discussion to add more details to the requirements for the above story.</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Test cases created by ChatGPT</td><td>Points to expand</td></tr>
</thead>
<tbody>
<tr>
<td>Precondition: Logged in as an authorised HR system user.</td><td>Are different levels of HR users required to add/edit this column?</td></tr>
<tr>
<td>Precondition: Employee table has an additional column for ‘Nationality’.</td><td>Separate requirements for DB tables.</td></tr>
<tr>
<td>Validate ‘Nationality’ data.</td><td>- What format column would this be? Free text, dropdown list?</td></tr>
<tr>
<td>- What about employees with multiple nationalities?</td><td></td></tr>
<tr>
<td>Work permit identification: Check if the system identifies the need for a work permit based on the combination of ’Country of Residence’ and ’Nationality’ for each employee.</td><td>This test case may lead to us having discussions about the scope of work:</td></tr>
<tr>
<td>- Should the system alert us when ‘Country of Residence’ and ‘Nationality’ differ? If yes, what sort of alert? Pop-up, email or another column? This may require another story.</td><td></td></tr>
<tr>
<td>- Logic of trigger. Should EU nations be ignored?</td><td></td></tr>
<tr>
<td>Error handling</td><td>- Is empty data allowed?</td></tr>
<tr>
<td>- Are access level tests required?</td></tr>
</tbody>
</table>
</div><p>So, by generating test cases, we created a checklist of items to verify for the story. If I were to create test cases, I would need focused time to understand the story, followed by at least ten minutes to work on the test cases. In comparison, ChatGPT generated them within five seconds, allowing the 'Three Amigos' participants to start their discussions immediately. By going through this checklist, we can identify additional business and technical requirements, helping to solidify the overall requirements further.</p>
<h2 id="heading-2-design">2. Design</h2>
<p>After the requirements are approved, they are transformed into blueprints for building the application. This may involve screen designs and prototypes to help visualise the requirements for the overall system and/or individual module designs.</p>
<p>Let’s assume that, after the ‘Three Amigos’ discussions, more details were added to the ‘Nationality’ column as below:</p>
<ul>
<li><p>‘Nationality’ is selected from the dropdown list.</p>
</li>
<li><p>Can choose only one nationality.</p>
</li>
<li><p>This is a mandatory field, a warning message is displayed if nothing is selected.</p>
</li>
<li><p>An alert pops up for a mismatch between ‘Country’ and ‘Nationality’</p>
</li>
<li><p>Other logic for the alert (such as ignoring EU nations) is out of scope for this sprint</p>
</li>
</ul>
<p>This UI design was drawn based on the above, improved requirements.</p>
<p><em>Sample UI design</em></p>
<p><img src="https://www.nearform.com/media/pages/digital-community/a-5-stage-guide-to-turbocharging-the-quality-assurance-process-with-generative-ai/0636db8c26-1728477029/blog-a-5-stage-guide-to-turbocharging-the-quality-assurance-process-with-generative-ai-sample-ui-design-1600x.png" alt="Sample UI Design" /></p>
<p>Let’s generate the test cases by pasting the UI design screenshot into ChatGPT and see if we can identify issues that need to be addressed.</p>
<p>This is what I asked:</p>
<p><img src="https://www.nearform.com/media/pages/digital-community/a-5-stage-guide-to-turbocharging-the-quality-assurance-process-with-generative-ai/ecc0647cf0-1728477157/blog-a-5-stage-guide-to-turbocharging-the-quality-assurance-process-with-generative-ai-chat-gpt-screenshot-1600x.png" alt="UI design screenshot on ChatGPT" /></p>
<p>It produced seven test cases, which are mostly similar to those for the story. Since this design was created after conducting a requirements analysis based on the earlier test cases, there should be fewer questions raised. I identified one test case that may require further discussion.</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Test case created by ChatGPT</td><td>Points to expand</td></tr>
</thead>
<tbody>
<tr>
<td>Accessibility and usability — confirm that the dropdown is accessible via keyboard navigation and can be expanded using the ‘Tab’ key.</td><td>- This will ensure developers use semantically meaningful HTML code.</td></tr>
<tr>
<td>- Further discussion may be required for colour contrast.</td></tr>
</tbody>
</table>
</div><p>The key point here is that since we got well-defined functional requirements during the analysis stage, we can focus on visual and usability requirements during the design review session.</p>
<h2 id="heading-3-implementation">3. Implementation</h2>
<p>The actual coding and building of software occurs in this stage. AI can now aid in developing code for both application and unit tests, but as my focus is on QA activities, I will not delve into it. I would like to examine what Test Engineers can do utilising GenAI during this stage.</p>
<ul>
<li><p>Create test tickets: You can ask the GenAI to create a test case against the user story and UI screenshot using the Gherkin language. As it produces test cases for core functionalities, you can spend more time planning and scripting for edge cases. I’m a fan of using Gherkin to create test tickets, as it forces you to break down the code to the smallest testable function.</p>
</li>
<li><p>Preparing work for automation tests: I used GenAI to create a page object and test files for <a target="_blank" href="https://www.nearform.com/digital-community/calling-software-test-engineers-is-it-time-to-replace-nightwatch-with-playwright/">Playwright</a> using the UI design screenshot.</p>
</li>
</ul>
<p>As I was adding automation test script codes to the repository, I used 'Continue,' a VSCode extension, instead of ChatGPT this time.</p>
<p>I pasted the design screenshot into the prompt section and asked ChatGPT to create a page object and test files.</p>
<p><img src="https://www.nearform.com/media/pages/digital-community/a-5-stage-guide-to-turbocharging-the-quality-assurance-process-with-generative-ai/9494a158ca-1728477383/blog-a-5-stage-guide-to-turbocharging-the-quality-assurance-process-with-generative-ai-object-and-test-files-1600x.png" alt="ChatGPT to create a page object and test files." /></p>
<p>Since this is a VSCode extension, it has a handy tool for adding generated code to your repository, as shown below:</p>
<ul>
<li><p>Apply to the current file.</p>
</li>
<li><p>Insert at the cursor.</p>
</li>
<li><p>Copy and paste the code.</p>
</li>
</ul>
<p><img src="https://www.nearform.com/media/pages/digital-community/a-5-stage-guide-to-turbocharging-the-quality-assurance-process-with-generative-ai/3130369bcc-1728477479/blog-a-5-stage-guide-to-turbocharging-the-quality-assurance-process-with-generative-ai-vscode-extension-1600x.png" alt="VSCode extension" /></p>
<p>You can see below what it generated.</p>
<p><em>Page object file</em></p>
<pre><code class="lang-js"><span class="hljs-comment">// employeePage.js</span>
<span class="hljs-keyword">const</span> { expect } = <span class="hljs-built_in">require</span>(<span class="hljs-string">'@playwright/test'</span>);

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">EmployeePage</span> </span>{
  <span class="hljs-keyword">constructor</span>(page) {
    <span class="hljs-built_in">this</span>.page = page;
    <span class="hljs-built_in">this</span>.employeeRows = page.locator(<span class="hljs-string">'tr.employee-row'</span>);
    <span class="hljs-built_in">this</span>.employeeId = page.locator(<span class="hljs-string">'td:nth-child(2)'</span>);
    <span class="hljs-built_in">this</span>.department = page.locator(<span class="hljs-string">'td:nth-child(3)'</span>);
    <span class="hljs-built_in">this</span>.employeeType = page.locator(<span class="hljs-string">'td:nth-child(4)'</span>);
    <span class="hljs-built_in">this</span>.country = page.locator(<span class="hljs-string">'td:nth-child(5)'</span>);
    <span class="hljs-built_in">this</span>.nationalityDropdown = page.locator(<span class="hljs-string">'td:nth-child(6) select'</span>);
  }

  <span class="hljs-keyword">async</span> getEmployeeCount() {
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.employeeRows.count();
  }

  <span class="hljs-keyword">async</span> getEmployeeId(rowIndex) {
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.employeeRows.nth(rowIndex).locator(<span class="hljs-string">'td:nth-child(2)'</span>).textContent();
  }

  <span class="hljs-keyword">async</span> getDepartment(rowIndex) {
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.employeeRows.nth(rowIndex).locator(<span class="hljs-string">'td:nth-child(3)'</span>).textContent();
  }

  <span class="hljs-keyword">async</span> getEmployeeType(rowIndex) {
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.employeeRows.nth(rowIndex).locator(<span class="hljs-string">'td:nth-child(4)'</span>).textContent();
  }

  <span class="hljs-keyword">async</span> getCountry(rowIndex) {
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.employeeRows.nth(rowIndex).locator(<span class="hljs-string">'td:nth-child(5)'</span>).textContent();
  }

  <span class="hljs-keyword">async</span> selectNationality(rowIndex, nationality) {
    <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.employeeRows.nth(rowIndex).locator(<span class="hljs-string">'td:nth-child(6) select'</span>).selectOption(nationality);
  }

  <span class="hljs-keyword">async</span> assertEmployeeDetails(rowIndex, expectedId, expectedDepartment, expectedEmployeeType, expectedCountry) {
    <span class="hljs-keyword">const</span> employeeId = <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.getEmployeeId(rowIndex);
    <span class="hljs-keyword">const</span> department = <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.getDepartment(rowIndex);
    <span class="hljs-keyword">const</span> employeeType = <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.getEmployeeType(rowIndex);
    <span class="hljs-keyword">const</span> country = <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.getCountry(rowIndex);

    expect(employeeId).toBe(expectedId);
    expect(department).toBe(expectedDepartment);
    expect(employeeType).toBe(expectedEmployeeType);
    expect(country).toBe(expectedCountry);
  }
}

<span class="hljs-built_in">module</span>.exports = { EmployeePage };
</code></pre>
<p><em>Test file</em></p>
<pre><code class="lang-js"><span class="hljs-comment">// employeeTest.spec.js</span>
<span class="hljs-keyword">const</span> { test, expect } = <span class="hljs-built_in">require</span>(<span class="hljs-string">'@playwright/test'</span>);
<span class="hljs-keyword">const</span> { EmployeePage } = <span class="hljs-built_in">require</span>(<span class="hljs-string">'./employeePage'</span>);

test.describe(<span class="hljs-string">'Employee Table Tests'</span>, <span class="hljs-function">() =&gt;</span> {
  <span class="hljs-keyword">let</span> employeePage;

  test.beforeEach(<span class="hljs-keyword">async</span> ({ page }) =&gt; {
    employeePage = <span class="hljs-keyword">new</span> EmployeePage(page);
    <span class="hljs-keyword">await</span> page.goto(<span class="hljs-string">'https://your-application.com/employee-table'</span>);
  });

  test(<span class="hljs-string">'should display employee table'</span>, <span class="hljs-keyword">async</span> () =&gt; {
    <span class="hljs-keyword">const</span> employeeCount = <span class="hljs-keyword">await</span> employeePage.getEmployeeCount();
    expect(employeeCount).toBeGreaterThan(<span class="hljs-number">0</span>);
  });

  test(<span class="hljs-string">'should display correct employee details'</span>, <span class="hljs-keyword">async</span> () =&gt; {
    <span class="hljs-keyword">await</span> employeePage.assertEmployeeDetails(<span class="hljs-number">0</span>, <span class="hljs-string">'0001'</span>, <span class="hljs-string">'HR'</span>, <span class="hljs-string">'Permanent'</span>, <span class="hljs-string">'USA'</span>);
    <span class="hljs-keyword">await</span> employeePage.assertEmployeeDetails(<span class="hljs-number">1</span>, <span class="hljs-string">'0002'</span>, <span class="hljs-string">'IT'</span>, <span class="hljs-string">'Contract'</span>, <span class="hljs-string">'Canada'</span>);
  });

  test(<span class="hljs-string">'should select nationality from dropdown'</span>, <span class="hljs-keyword">async</span> () =&gt; {
    <span class="hljs-keyword">await</span> employeePage.selectNationality(<span class="hljs-number">1</span>, <span class="hljs-string">'American'</span>);
    <span class="hljs-comment">// Add assertions to verify the selected nationality</span>
  });
});
</code></pre>
<p>It’s usable, but definitely needs some time to review it. However, I found this saves quite a bit of time compared to manually creating these files — it’s a good base to work from. This also reminds me to inform developers to add test IDs and roles for accessibility (a11y) to each element!</p>
<h2 id="heading-4-testing">4. Testing</h2>
<p>Different types of tests are executed to ensure that the intended functionalities are implemented. Unintended behaviours are reported during this stage.</p>
<p>All QA preparation work has been aided and enhanced by GenAI at every stage up until this point. It's now time for QA Engineers to use some elbow grease and let their experience and skills shine!</p>
<ul>
<li><p>Execute functional testing: Add results and evidence to test tickets. Raise bugs if necessary.</p>
</li>
<li><p>Exploratory testing: As Test Engineers may free up time by utilising GenAI, they can perform experience-based exploratory testing. This may help catch issues that scripted test cases cannot.</p>
</li>
<li><p>Complete automation test script: Identify what test to automate then update and add codes to the base files which were created by GenAI.</p>
</li>
<li><p>Create a test report: This is something GenAI can do. I created a sample bug report asking ChatGPT as below.</p>
</li>
</ul>
<p><img src="https://www.nearform.com/media/pages/digital-community/a-5-stage-guide-to-turbocharging-the-quality-assurance-process-with-generative-ai/7e04e7d31e-1728477672/blog-a-5-stage-guide-to-turbocharging-the-quality-assurance-process-with-generative-ai-sample-bug-1600x.png" alt="sample bug report" /></p>
<p>This command generated decent reports, including 'Description' and 'Steps to Reproduce.' Though I misspelt "width", it correctly assumed and generated the report. A few tweaks were needed, but, again, it saves time, allowing Test Engineers to focus on what truly matters at this stage — performing various types of tests and applying their experience to ensure the quality of newly implemented functionalities.</p>
<h2 id="heading-5-maintenance">5. Maintenance</h2>
<p>The maintenance stage occurs after the deployment of production. It involves improving, adapting and evolving the software. New requirements may arise from real user feedback. If production bugs are identified, they will be fixed, tested and released. Whether it involves fixing production bugs or implementing new requirements, the steps of SDLC stages 1-4 should be followed.</p>
<p>During the maintenance stage, automated tests need to be updated and maintained. Since the application is now in production, you can provide the URL to the GenAI to generate additional Playwright tests. This allows the AI to capture real locators.</p>
<p>For instance, let’s say a new business requirement states that 'Check' is no longer an available option for the payment method. In this case, additional tests need to be created. I used ‘Continue’ as shown below:</p>
<p><img src="https://www.nearform.com/media/pages/digital-community/a-5-stage-guide-to-turbocharging-the-quality-assurance-process-with-generative-ai/410233a5c7-1728477842/blog-a-5-stage-guide-to-turbocharging-the-quality-assurance-process-with-generative-ai-platwright-test-1600x.png" alt="Playwright tests" /></p>
<p>And it generated the below test.</p>
<pre><code class="lang-js"><span class="hljs-comment">// paymentMethodColumnTest.spec.js</span>

<span class="hljs-keyword">const</span> { test, expect } = <span class="hljs-built_in">require</span>(<span class="hljs-string">'@playwright/test'</span>);

test.describe(<span class="hljs-string">'Payment Method Column Test'</span>, <span class="hljs-function">() =&gt;</span> {
  <span class="hljs-keyword">let</span> page;

  test.beforeAll(<span class="hljs-keyword">async</span> ({ browser }) =&gt; {
    page = <span class="hljs-keyword">await</span> browser.newPage();
    <span class="hljs-keyword">await</span> page.goto(<span class="hljs-string">'https://www.ag-grid.com/example-hr/'</span>);
  });

  test.afterAll(<span class="hljs-keyword">async</span> () =&gt; {
    <span class="hljs-keyword">await</span> page.close();
  });

  test(<span class="hljs-string">'should not contain "Check" in Payment Method column'</span>, <span class="hljs-keyword">async</span> () =&gt; {
    <span class="hljs-comment">// Get all the cells in the Payment Method column</span>
    <span class="hljs-keyword">const</span> paymentMethodCells = <span class="hljs-keyword">await</span> page.$$eval(<span class="hljs-string">'div[col-id="paymentMethod"] div.ag-cell-value'</span>, <span class="hljs-function">(<span class="hljs-params">cells</span>) =&gt;</span>
      cells.map(<span class="hljs-function">(<span class="hljs-params">cell</span>) =&gt;</span> cell.textContent.trim())
    );

    <span class="hljs-comment">// Check if none of the cells contain the value "Check"</span>
    expect(paymentMethodCells).not.toContain(<span class="hljs-string">'Check'</span>);
  });
});
</code></pre>
<p>It looks OK but, unfortunately, this did not correctly verify the column. Again, you can use it as a base and update it.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>You may already be practising 'Shift Left', but, as demonstrated above, this approach can be further accelerated with GenAI. By scrutinising requirements and designs more thoroughly in the early stages of the SDLC, more time is freed up for Test Engineers to focus on higher-value tasks like experience-based exploratory testing and scripting efficient automated tests.</p>
<p>While GenAI offers significant support, Test Engineers must still fully understand and be capable of manually performing all QA activities. Exercising good judgment is crucial in deciding which tasks can be delegated to AI and which require personal attention.</p>
<p>GenAI can serve as governance throughout each stage of the SDLC, ensuring that processes are followed. By generating test cases, GenAI provides a checklist that acts as a safeguard before progressing to the next stage. A good checklist is one of the simplest yet most effective tools for maintaining quality — not only in software but in nearly any field. I recall reading an article about how the use of a surgical checklist cut infection and death rates by half (ref: <a target="_blank" href="https://www.ncbi.nlm.nih.gov/pmc/articles/PMC5885453/">National Library of Medicine</a>).</p>
<p>Could a similar approach reduce production bugs by half if we implement test cases at every stage of the SDLC? While it's hard to quantify, I believe that whether they are UI/UX, requirements, or functional bugs, this approach will undoubtedly reduce the number of issues that slip through to production.</p>
]]></content:encoded></item><item><title><![CDATA[Calling Software Test Engineers! Is it time to replace Nightwatch with Playwright?]]></title><description><![CDATA[By Aska Cloke I have worked with various automation tools throughout my career — starting with QuickTest Professional (QTP), now called Unified Functional…]]></description><link>https://nearform.hashnode.dev/calling-software-test-engineers-is-it-time-to-replace-nightwatch-with-playwright</link><guid isPermaLink="true">https://nearform.hashnode.dev/calling-software-test-engineers-is-it-time-to-replace-nightwatch-with-playwright</guid><category><![CDATA[night watch]]></category><category><![CDATA[Software Engineering]]></category><category><![CDATA[automation tools]]></category><category><![CDATA[playwright]]></category><category><![CDATA[test engineer]]></category><dc:creator><![CDATA[Nearform]]></dc:creator><pubDate>Wed, 02 Oct 2024 14:57:42 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1727429856196/e2119e8c-94c0-4f1c-a650-3b5e5b9ac443.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>By Aska Cloke</p>
<h2 id="heading-we-spare-you-the-pain-of-assessing-these-two-automation-tools-by-looking-at-how-they-compare-to-each-other">We spare you the pain of assessing these two automation tools by looking at how they compare to each other</h2>
<p>I have worked with various automation tools throughout my career — starting with QuickTest Professional (QTP), now called Unified Functional Testing <a target="_blank" href="https://www.microfocus.com/documentation/silk-central/200/en/silkcentral-help-en/GUID-531809BA-688F-41D5-BDB2-FCE786A284CE.html">(UFT)</a>, and, most recently, <a target="_blank" href="https://playwright.dev/">Playwright</a>. One of my least favourite tools in the past was Nightwatch. However, it seems that <a target="_blank" href="https://nightwatchjs.org/">Nightwatch</a> has improved significantly, so I thought I'd give it another look and see how it stands up to Playwright today!</p>
<p>This article is for Software Test Engineers. I hope I can spare you some pain in comparing tools and maybe even share some practical knowledge about these modern testing frameworks. Empowered with that knowledge, we can all make informed decisions when choosing a testing framework.</p>
<h2 id="heading-built-in-functionalities">Built-in functionalities</h2>
<p>When I first used Nightwatch back in 2016/2017, I don’t recall any of the below functionalities being built-in. Instead, we had to code them explicitly. Fast forward to 2024, and these built-in features are now available in both Nightwatch and Playwright.</p>
<h3 id="heading-1-auto-wait-and-auto-retries">1. Auto wait and auto retries</h3>
<p>Both Nightwatch and Playwright automatically wait for elements to appear before attempting to interact with them. For example, if your test requires you to enter a username into an input box but the screen takes time to load, both tools will keep checking for the input box to be available until they time out. As a result, you can remove the 'wait until' code for potentially slow-loading pages, making the tests clearer and easier to read. The default timeout setting is five seconds for Nightwatch and 30 seconds for Playwright, but this can be reconfigured in the configuration file.</p>
<p>The automatic retry feature also simplifies the test script. When an assertion fails, it automatically re-asserts the failed line a specified number of times, which can be configured in the configuration file.</p>
<h3 id="heading-2-handling-of-pop-up-messages">2. Handling of pop-up messages</h3>
<p>Both Nightwatch and Playwright can handle pop-up messages by explicitly waiting for them. However, Playwright takes it a step further by making it easier to handle pesky, unpredictable pop-ups that appear at different times using <em>page.addLocateHandler()</em>. When an overlay is detected at any point during the test, Playwright first calls the handler, and then proceeds with the specified action to remove the overlay. This feature is unique to Playwright.</p>
<pre><code class="lang-typescript">test(<span class="hljs-string">'pop-up test'</span>, <span class="hljs-keyword">async</span>({page}) =&gt; {
    <span class="hljs-keyword">await</span> page.goto(<span class="hljs-string">'https://www.techlistic.com/p/selenium-practice-form.html'</span>);
    <span class="hljs-keyword">await</span> page.addLocatorHandler(
      page.getByRole(<span class="hljs-string">'heading'</span>, { name: <span class="hljs-string">'Privacy &amp; Transparency'</span> }),
      <span class="hljs-keyword">async</span> () =&gt; {
          <span class="hljs-comment">// This line is executed whenever overlay is detected.</span>
        <span class="hljs-keyword">await</span> page.click(<span class="hljs-string">'#ez-accept-necessary'</span>);  
      });

      <span class="hljs-keyword">await</span> expect(page.getByRole(<span class="hljs-string">'heading'</span>, { name: <span class="hljs-string">'Demo Automation Practice Form'</span> })).toBeVisible();
      <span class="hljs-keyword">await</span> page.locator(<span class="hljs-string">'input[name="firstname"]'</span>).fill(<span class="hljs-string">'test-firstname'</span>);
  })
</code></pre>
<p><em>Playwright addLocateHander() example</em></p>
<h3 id="heading-3-reporting-tool">3. Reporting tool</h3>
<p>Both of these automation tools come with stylish HTML reports and the frameworks add screenshots when a test fails. However, I found Nightwatch's report easier to navigate, as everything can be viewed on one screen. With Playwright, you have to navigate back and forth to check individual test results.</p>
<h4 id="heading-nightwatch-html-report"><strong>Nightwatch HTML report</strong></h4>
<p><img src="https://www.nearform.com/media/pages/digital-community/calling-software-test-engineers-is-it-time-to-replace-nightwatch-with-playwright/6f6afcf2a8-1727254744/calling-software-test-engineers-is-it-time-to-replace-nightwatch-with-playwright-nightwatch-html-report-1600x.png" alt="Nightwatch HTML report screenshot" /></p>
<h4 id="heading-playwright-html-report"><strong>Playwright HTML report</strong></h4>
<p><img src="https://www.nearform.com/media/pages/digital-community/calling-software-test-engineers-is-it-time-to-replace-nightwatch-with-playwright/e8235713d5-1727255046/calling-software-test-engineers-is-it-time-to-replace-nightwatch-with-playwright-playwright-html-report-1600x.png" alt="Playwright HTML report" /></p>
<p>You have to open the individual test to see the details</p>
<p><img src="https://www.nearform.com/media/pages/digital-community/calling-software-test-engineers-is-it-time-to-replace-nightwatch-with-playwright/6f1b5a763d-1727255134/calling-software-test-engineers-is-it-time-to-replace-nightwatch-with-playwright-playwright-html-report-individual-test-1600x.png" alt="playwright-html-report-individual-test" /></p>
<h3 id="heading-4-screenshots">4. Screenshots</h3>
<p>Both Nightwatch and Playwright can take screenshots explicitly in code. Also, via a config file, you can enable automatic screenshots for failing tests.</p>
<h4 id="heading-nightwatch"><strong>Nightwatch</strong></h4>
<p>Test file</p>
<pre><code class="lang-js">browser.saveScreenshot(<span class="hljs-string">'./screenshots/screenshot.png'</span>)
</code></pre>
<p>nightwatch.conf.js</p>
<pre><code class="lang-js">test_settings: {
    <span class="hljs-attr">default</span>: {
      <span class="hljs-attr">screenshots</span>: {
        <span class="hljs-attr">enabled</span>: <span class="hljs-literal">true</span>,
        <span class="hljs-attr">path</span>: <span class="hljs-string">'screens'</span>,
        <span class="hljs-attr">on_failure</span>: <span class="hljs-literal">true</span>
      },
</code></pre>
<h4 id="heading-playwright"><strong>Playwright</strong></h4>
<p>Test file</p>
<pre><code class="lang-js"><span class="hljs-keyword">await</span> page.screenshot({ <span class="hljs-attr">path</span>: <span class="hljs-string">'screenshot.png'</span> });
</code></pre>
<p>playwright.config.js</p>
<pre><code class="lang-js">use: {
    <span class="hljs-attr">screenshot</span>: <span class="hljs-string">'only-on-failure'</span>,
  },
</code></pre>
<h3 id="heading-5-test-recordercode-generator">5. Test recorder/code generator</h3>
<p>Nightwatch uses a Chrome <a target="_blank" href="https://chromewebstore.google.com/detail/nightwatch-chrome-recorde/nhbccjfogdgkahamfohokdhcnemjafjk">extension</a> that can automatically generate tests by recording user actions. From the DevTools, you can record multiple tests and export them to Nightwatch. While it can be a bit clunky to use, it allows you to record actions and add assertions. However, as seen in the exported script, the locators it captures are not very optimised (we should always choose ‘asserting by role’ over ‘selector’ to test accessibility).</p>
<p>Chrome recording</p>
<p><img src="https://www.nearform.com/media/pages/digital-community/calling-software-test-engineers-is-it-time-to-replace-nightwatch-with-playwright/110f921081-1727255776/calling-software-test-engineers-is-it-time-to-replace-nightwatch-with-playwright-chrome-recording-1600x.png" alt="Chrome recording" /></p>
<p>Exported script</p>
<pre><code class="lang-js">describe(<span class="hljs-string">"nightwatch"</span>, <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params"></span>) </span>{
it(<span class="hljs-string">"tests nightwatch"</span>, <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">browser</span>) </span>{
  browser.windowRect({<span class="hljs-attr">width</span>: <span class="hljs-number">1705</span>, <span class="hljs-attr">height</span>: <span class="hljs-number">693</span>})
  .navigateTo(<span class="hljs-string">"https://www.nearform.com/"</span>)
  .click(<span class="hljs-string">"ul.nav_right &gt; li:nth-of-type(2) &gt; a"</span>)
  .click(<span class="hljs-string">"#firstname-13c6a12c-2dbc-4455-a6cc-694b8bf39cb8"</span>)
  .setValue(<span class="hljs-string">"#firstname-13c6a12c-2dbc-4455-a6cc-694b8bf39cb8"</span>, <span class="hljs-string">"test"</span>)
  .click(<span class="hljs-string">"div.hs_submit input"</span>)

        .waitForElementVisible(<span class="hljs-string">"text/Please complete all"</span>, <span class="hljs-function"><span class="hljs-keyword">function</span>(<span class="hljs-params">result</span>) </span>{
          <span class="hljs-keyword">if</span> (result.value) {
            <span class="hljs-literal">undefined</span>
          }
        })

  .end();
  });
});
</code></pre>
<p>In comparison, Playwright has its own test recorder called codegen, which can be launched by running the below code:</p>
<pre><code class="lang-js">npx playwright codegen
</code></pre>
<p>Test code is generated in real-time as you navigate through the page. It is intuitive and quite user-friendly. You can record actions, capture page locators, and add assertions. Additionally, it supports device emulation and allows you to specify geolocation, language, and time zone. The power of this feature is hard to describe. Check out all of the features <a target="_blank" href="https://playwright.dev/docs/codegen">here</a>.</p>
<p><img src="https://www.nearform.com/media/pages/digital-community/calling-software-test-engineers-is-it-time-to-replace-nightwatch-with-playwright/e8e6c59b0a-1727255933/calling-software-test-engineers-is-it-time-to-replace-nightwatch-with-playwright-codegen-gif-1600x.gif" alt /></p>
<h2 id="heading-vscode-extension">VSCode extension</h2>
<p>Running and scripting tests gets a whole lot easier with a VSCode extension. Both Nightwatch and Playwright have extensions, but they have slightly different functionalities. Let’s compare them.</p>
<h4 id="heading-nightwatch-extension"><strong>Nightwatch extension</strong></h4>
<p>Test Explorer</p>
<ul>
<li>Nightwatch tests integrate directly into VSCode's built-in 'Test Explorer' into a dedicated section. You can execute tests from this section or directly from a test file.</li>
</ul>
<p>Browsers</p>
<ul>
<li><p>Choose ‘Browser’ from the ‘Nightwatch Environment’ section.</p>
</li>
<li><p>You can only select one browser at a time from this section.</p>
</li>
</ul>
<p>Settings</p>
<ul>
<li><p>Select ‘Headless’ or ‘Headed’ mode.</p>
</li>
<li><p>Open report automatically after tests have run.</p>
</li>
<li><p>Set the number of parallels.</p>
</li>
</ul>
<p><img src="https://www.nearform.com/media/pages/digital-community/calling-software-test-engineers-is-it-time-to-replace-nightwatch-with-playwright/39918e0cc5-1727256156/calling-software-test-engineers-is-it-time-to-replace-nightwatch-with-playwright-nightwatch-extension-setting-1600x.png" alt="nightwatch-extension-setting" /></p>
<h4 id="heading-playwright-extension"><strong>Playwright extension</strong></h4>
<p>Test Explorer</p>
<ul>
<li>All executable Playwright tests appear in this section.</li>
</ul>
<p>Browsers</p>
<ul>
<li>You can select multiple browsers.</li>
</ul>
<p>Settings</p>
<ul>
<li><p>Select ‘Headless’ or ‘Headed’ mode.</p>
</li>
<li><p>Launch ‘Trace Viewer’, which enables you to follow tests step by step along with screenshots and information you can get from a browser dev tool, such as network trace. This makes debugging easier.</p>
</li>
</ul>
<p><img src="https://www.nearform.com/media/pages/digital-community/calling-software-test-engineers-is-it-time-to-replace-nightwatch-with-playwright/dc98d7b360-1727256225/calling-software-test-engineers-is-it-time-to-replace-nightwatch-with-playwright-playwright-extension-basic-test-1600x.png" alt="playwright-extension-basic-test" /></p>
<p>Tools</p>
<ul>
<li>One feature that sets Playwright apart from Nightwatch is its tool for selecting locators. I found the 'Pick Locator' feature to be very handy. You can point to the element you need, and the page locator is captured into VSCode. By pressing the Enter key on the captured locator, it is copied and is then ready to be pasted into your code.</li>
</ul>
<p><img src="https://www.nearform.com/media/pages/digital-community/calling-software-test-engineers-is-it-time-to-replace-nightwatch-with-playwright/82c46274ee-1727256296/calling-software-test-engineers-is-it-time-to-replace-nightwatch-with-playwright-pick-locator-1600x.png" alt="pick-locator" /></p>
<ul>
<li>The ‘Record New’ functionality is almost exactly the same as codegen. The only difference is that it creates a new test file, then the test code is generated there as you navigate through the screen.</li>
</ul>
<p>I rarely use codegen because I can do most jobs from the VSCode extension tools. If you haven’t used them before, I highly recommend giving them a try.</p>
<h2 id="heading-language-support-syntax-comparison-and-debugging">Language support, syntax comparison and debugging</h2>
<p>Below, we look at language support, syntax and debugging for Nightwatch and Playwright.</p>
<h3 id="heading-1-language-support">1. Language support</h3>
<p>Nightwatch supports JavaScript and TypeScript, making it ideal for teams working within a JavaScript ecosystem. Playwright, however, offers broader language support, including Java, Python, and .NET, in addition to JavaScript and TypeScript.</p>
<h3 id="heading-2-syntax-comparison">2. Syntax comparison</h3>
<p>Up until v2, Nightwatch uses a traditional callback or promise style. Each command, like <code>.findByRole</code> or <code>.url</code>, relies on a callback function that contains assertions or further actions. WebDriver is used to execute tests.</p>
<p>*<em>However, v3 (July 2023) introduced synchronous syntax.</em></p>
<p>Nightwatch test — up to v2</p>
<pre><code class="lang-js"><span class="hljs-built_in">module</span>.exports = {
    <span class="hljs-string">'nightwatch - assert filter and title'</span>: <span class="hljs-function"><span class="hljs-keyword">function</span>(<span class="hljs-params">browser</span>)</span>{
        browser.url(browser.launch_url);
        browser
            .element.findByRole(<span class="hljs-string">'button'</span>, {<span class="hljs-attr">name</span>: <span class="hljs-string">'Reject All'</span>}).click();
        browser
            .assert.visible(<span class="hljs-string">'input[placeholder="Filter any column..."]'</span>)
            .assert.titleEquals(<span class="hljs-string">'Demo - Performance Grid'</span>);
    }
}
</code></pre>
<p>Nightwatch using async/await (from v3)</p>
<pre><code class="lang-js"><span class="hljs-built_in">module</span>.exports = {
    <span class="hljs-string">'nightwatch - assert filter and title'</span>: <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span>(<span class="hljs-params">browser</span>) </span>{
    <span class="hljs-keyword">await</span> browser.url(browser.launch_url);
    <span class="hljs-keyword">await</span> browser.element.findByRole(<span class="hljs-string">'button'</span>, { <span class="hljs-attr">name</span>: <span class="hljs-string">'Reject All'</span> }).click();
    <span class="hljs-keyword">await</span> browser.assert.visible(<span class="hljs-string">'input[placeholder="Filter any column..."]'</span>);
    <span class="hljs-keyword">await</span> browser.assert.titleEquals(<span class="hljs-string">'Demo - Performance Grid'</span>);
    }
};
</code></pre>
<p>Playwright uses async/await syntax and tests are executed directly in the browser.</p>
<pre><code class="lang-typescript">test(<span class="hljs-string">'playwright - assert filter and title'</span>, <span class="hljs-keyword">async</span> ({ page }) =&gt; {
    <span class="hljs-keyword">await</span> page.goto(<span class="hljs-string">'/example'</span>);
    <span class="hljs-keyword">await</span> page.getByRole(<span class="hljs-string">'button'</span>, { name: <span class="hljs-string">'Reject All'</span> }).click();
    <span class="hljs-comment">// break for debug here</span>
    <span class="hljs-keyword">await</span> expect(page.getByPlaceholder(<span class="hljs-string">'Filter any column...'</span>)).toBeVisible();
    <span class="hljs-keyword">await</span> expect(page).toHaveTitle(<span class="hljs-string">'Demo - Performance Grid'</span>)
  });
</code></pre>
<h3 id="heading-3-debugging">3. Debugging</h3>
<p>Previously, Nightwatch only supported callback-based syntax, which made debugging more challenging. However, this was addressed in mid-2023, making debugging simpler and allowing for a more meaningful comparison between Nightwatch and Playwright. Let’s compare what happens when you try to debug the above sample code.</p>
<ol>
<li><p>Put in a break line, then click the ‘Reject All’ button.</p>
</li>
<li><p>Execute tests in debug mode.</p>
</li>
<li><p>The test reaches the break line.</p>
</li>
</ol>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Nightwatch up to v2</td><td>Playwright and Nightwatch v3 with async/await syntax</td></tr>
</thead>
<tbody>
<tr>
<td>- The base URL is not launched.</td><td>- The base URL is launched.</td></tr>
<tr>
<td>- Step over to the next line — nothing happens.</td><td>- Step over to the next line — line is executed.</td></tr>
<tr>
<td>- Step over to the last bracket, then you have to go through all chained events to see the scripted action happen.</td><td>- You can see the actions in the browser step by step.</td></tr>
</tbody>
</table>
</div><h2 id="heading-conclusion">Conclusion</h2>
<p>Nightwatch has significantly improved over the years, and I am impressed by how user-friendly it has become. However, I still prefer Playwright over Nightwatch due to some limitations that are difficult to overcome. For instance, Nightwatch requires the setup and maintenance of WebDriver. Up until mid-2023, Nightwatch only supported callback-based syntax, which made debugging more challenging. Ease of debugging is crucial when determining the cause of a test failure, as you need to quickly ascertain whether it is a valid failure or an error in the test script. While Nightwatch now supports async/await syntax, it came a bit too late, as Playwright has been using this feature since 2020.</p>
<p>Playwright also supports more languages, making it adaptable to a wider range of development stacks. The framework is easier to maintain since there is no need for a separate WebDriver, and tests are executed directly in the web browser. Its code is simpler to script and read, which makes the test development process more efficient. Thus, I’ll be sticking with Playwright until another new tool emerges for evaluation.</p>
]]></content:encoded></item><item><title><![CDATA[Top tips from Nearform’s website performance optimisation checklist]]></title><description><![CDATA[By Paul Negrutiu In today's fast-paced digital landscape, the need for top speed and performance for your website has never been more critical. Whether…]]></description><link>https://nearform.hashnode.dev/top-tips-from-nearforms-website-performance-optimisation-checklist</link><guid isPermaLink="true">https://nearform.hashnode.dev/top-tips-from-nearforms-website-performance-optimisation-checklist</guid><category><![CDATA[Website performance]]></category><category><![CDATA[Next.js]]></category><category><![CDATA[Performance Optimization]]></category><category><![CDATA[Web Development]]></category><category><![CDATA[website optimization]]></category><dc:creator><![CDATA[Nearform]]></dc:creator><pubDate>Fri, 23 Aug 2024 12:17:44 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1724415064720/8d8e1fc9-8ec5-48e3-996c-8d28a73d86f5.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>By Paul Negrutiu</p>
<h2 id="heading-we-share-essential-steps-every-developer-should-follow-to-ensure-their-website-performs-at-its-best-with-specific-insights-for-nextjs">We share essential steps every developer should follow to ensure their website performs at its best, with specific insights for Next.js</h2>
<p>In today's fast-paced digital landscape, the need for top speed and performance for your website has never been more critical. Whether it's to capture user attention, improve search engine optimisation (SEO) or simply provide a seamless browsing experience, performance optimisation is a priority for website developers across all stacks.</p>
<p>At Nearform we've had the privilege of working on a variety of projects, ranging from building apps from the ground up to critical interventions in ongoing developments. This extensive experience has given us deep insights into performance optimisation. We frequently collaborate with clients who demand exceptional scalability and performance, and our proven strategies have consistently delivered outstanding results.</p>
<h2 id="heading-website-performance-is-a-fundamental-aspect-of-user-satisfaction-and-engagement">Website performance is a fundamental aspect of user satisfaction and engagement</h2>
<p>Lately, many developers have made Next.js their web development framework of choice. This is due to its ability to enhance SEO and improve search engine rankings, driving more traffic and engagement to their website.</p>
<p>The rise in popularity of Next.js can be attributed in part to its robust performance capabilities. With built-in server-side rendering (SSR) and static site generation (SSG), Next.js empowers developers to create blazing-fast web applications without compromising on functionality or user experience.</p>
<p>However, even the most powerful tools require fine-tuning to unleash their full potential. This guide will cover general optimisation techniques, with a specific focus on Next.js, to help you achieve superior performance and better rankings.</p>
<p>As we dive into the details of optimising Next.js website performance, it's essential to recognise that speed is not just a technical concern — it's a fundamental aspect of user satisfaction and engagement. Users expect websites to load quickly and seamlessly, regardless of the underlying technology stack, these are some of the things to be aware of:</p>
<ul>
<li><p><a target="_blank" href="https://blog.kissmetrics.com/wp-content/uploads/2011/04/loading-time.pdf">47% of users</a> expect a page to load in under 2 seconds</p>
</li>
<li><p>100ms in latency <a target="_blank" href="https://www.gigaspaces.com/blog/amazon-found-every-100ms-of-latency-cost-them-1-in-sales">costs 1% in sales</a></p>
</li>
<li><p>Load times over 1s for a mobile site <a target="_blank" href="https://www.thinkwithgoogle.com/marketing-strategies/app-and-mobile/mobile-page-speed-new-industry-benchmarks/">increase visitor bounce rate by 90%</a></p>
</li>
</ul>
<p>While our focus is on Next.js, it's worth noting that many of the performance optimisation techniques we'll discuss can be applied across various technology stacks —- irrespective of the framework, the principles of performance optimisation remain largely consistent.</p>
<h2 id="heading-quick-tips-from-nearforms-website-optimisation-checklist">Quick tips from Nearform’s website optimisation checklist</h2>
<p>Drawing from our past experiences, we've developed a checklist that can serve as a go-to guide for website optimisation. This checklist encapsulates some of the essential steps every developer should follow to ensure their website performs at its best. While we include general optimisation techniques, we also provide specific insights for Next.js.</p>
<p>Here are some quick tips from our checklist to get you started, and remember, our team at Nearform is always ready to provide expert guidance tailored to your specific needs.</p>
<h3 id="heading-must-do-set-up-effective-monitoring">Must-do: Set up effective monitoring</h3>
<p>Before delving into the intricacies of performance optimisation, it's essential to establish a robust monitoring system tailored to your application. Effective monitoring not only allows you to track the impact of optimisations over time but also facilitates the identification of potential bottlenecks, empowering you to make informed decisions for further performance enhancements.</p>
<p>For real-time monitoring and visualisation, leveraging tools like Prometheus and Grafana is paramount. Prometheus excels in collecting metrics from both your application and infrastructure, while Grafana offers comprehensive visualisation and analysis capabilities. Together, they enable you to scrutinise crucial performance indicators and help with identifying performance bottlenecks and resource constraints, paving the way for targeted optimisations.</p>
<p>In addition to real-time monitoring, investing in performance monitoring tools like NewRelic or Datadog provides invaluable insights into your application's performance at a deeper level. By analysing this data, you can fine-tune your application's performance, ensuring seamless user experiences across various scenarios.</p>
<h3 id="heading-vital-check-perfect-your-caching-setup">Vital check: Perfect your caching setup</h3>
<p>Caching is one of the most effective ways to improve the performance of your web application. When implementing caching, it's essential to identify what resources and data should be cached, how long should those be cached for and when the cache should be refreshed.</p>
<p>When dealing with large volumes of data, slow performance can be a common issue. Even fetching cached data can unexpectedly take a significant amount of time. Our experience reveals the efficacy of compressing data (utilising formats like gzip or brotli) before caching, as it can notably enhance load times. It's worth noting that certain caching solutions offer this compression option, presenting a potential game-changer. Keeping a vigilant eye on such features could yield significant performance improvements.</p>
<h3 id="heading-necessary-validation-confirm-connection-reuse">Necessary validation: Confirm connection reuse</h3>
<p>Connection reuse is a crucial strategy for optimising the performance of web applications. By reusing existing connections, instead of establishing new ones for each request, you can reduce latency, and improve throughput and the overall speed of your application. This is particularly important for applications that make numerous HTTP requests or database queries.</p>
<p>Reusing connections eliminates the need for repeated TCP handshakes, reducing the time it takes to establish a connection and start data transfer while also reducing the computational and memory overhead associated with establishing and tearing down connections, leading to more efficient resource utilisation.</p>
<p>Ensure that your web server is configured to support Keep-Alive connections. If your application makes frequent database queries, consider using connection pooling to reuse database connections. When making HTTP requests from your Next.js application, opt for HTTP clients that support Keep-Alive. For instance, Node.js's HTTP and HTTPS modules inherently support Keep-Alive, while some third-party libraries, such as axios, may require explicit configuration for this feature. Set an appropriate timeout value to strike a balance between reusing connections efficiently and avoiding the risks of keeping idle connections open for too long.</p>
<h3 id="heading-optimise-your-cdn-strategy">Optimise your CDN strategy</h3>
<p>Content Delivery Networks (CDNs) play a vital role in improving the performance and reliability of web applications by distributing content closer to users.</p>
<p>Select a CDN provider that suits your needs. Popular options include Cloudflare, AWS CloudFront, Akamai and Fastly. Each provider offers different features and pricing models, so choose one that aligns with your performance and budget requirements. Enable compression on your CDN to further reduce the size of transmitted data. Most CDNs support Gzip and Brotli compression. Use HTTP headers to control how and when your assets are cached.</p>
<h3 id="heading-data-efficiency-optimise-nextdata">Data efficiency: Optimise <code>__NEXT_DATA__</code></h3>
<p>The <code>__NEXT_DATA__</code> object is a global variable injected by Next.js into every page on the client side to help with faster rendering and it can grow in size extremely fast (<a target="_blank" href="https://nextjs.org/docs/pages/building-your-application/data-fetching">read more</a>).</p>
<p>Often times in cases when Next.js apps have performance issues, <code>__NEXT_DATA__</code> is a good place to focus for performance improvements. While the <code>__NEXT_DATA__</code> object in Next.js is a powerful tool, misusing its capabilities can lead to serious performance penalties. The key thing here is to only keep crucial and essential data about the page in this object.</p>
<p>Try to review the data and remove any piece of data that is not needed for the initial rendering. We’ve seen huge improvements just by double-checking for duplicate data or removing certain properties that weren’t needed initially (and you definitely have some of those).</p>
<h3 id="heading-picture-perfect-ensuring-ongoing-image-optimisation">Picture perfect: Ensuring ongoing image optimisation</h3>
<p><a target="_blank" href="https://developer.chrome.com/docs/lighthouse/overview">Lighthouse</a>, is an open-source automated tool for measuring website performance and quality. It often flags images as significant performance issues, especially on mobile. Common problems include improperly sized and unoptimised images. To address these issues, the Next.js next/image component is highly effective:</p>
<ul>
<li><p>Automatic optimisation: Images are optimised based on device characteristics, reducing bandwidth and improving CLS metrics.</p>
</li>
<li><p>Lazy loading: Images load as they enter the viewport, enhancing initial page load times.</p>
</li>
<li><p>Modern formats: Supports WebP and AVIF formats, which offer better compression and quality.</p>
</li>
<li><p>Responsive images: Automatically selects appropriate sizes for various screen sizes.</p>
</li>
<li><p>Placeholders: Provides visual feedback during loading, improving perceived performance.</p>
</li>
</ul>
<h3 id="heading-future-proof-maintaining-the-latest-in-nextjs">Future-proof: Maintaining the latest in Next.js</h3>
<p>Maintaining an up-to-date Next.js application is crucial for ensuring optimal performance. Regular updates bring numerous benefits, including performance enhancements, security patches, and new features.</p>
<p>Each Next.js release often includes optimisations that enhance performance. These updates can reduce build times, improve server-side rendering speed, and enhance client-side performance.</p>
<h3 id="heading-cut-the-clutter-removing-unused-jscss">Cut the clutter: Removing unused JS/CSS</h3>
<p>Eliminating unused JavaScript and CSS is a key strategy for optimising the performance of your Next.js application. Removing unused code reduces the overall size of your application, leading to quicker load times and a more responsive user experience.</p>
<p>Metrics such as First Contentful Paint (FCP), Time to Interactive (TTI), and Largest Contentful Paint (LCP) improve when there is less code to parse, compile, and execute. Smaller files are more efficient to cache, making repeated visits faster and reducing server load.</p>
<p>What can you do? Analyse your bundle and get rid of unnecessary code; make sure the project is set up to use tree shaking, dynamic imports can help in certain situations.</p>
<h2 id="heading-building-performance-piece-by-piece">Building performance: piece by piece</h2>
<p>Every project is unique and fine-tuning an application’s performance will be unique to that application. The above list serves as a guide to follow to shave off some big wins when it comes to performance tuning. The most important thing is to have a strategy and improve the performance piece by piece.</p>
]]></content:encoded></item><item><title><![CDATA[How to create a GenAI agent using Semantic Kernel]]></title><description><![CDATA[By Marcelo Ferreira In this article, we explore what Microsoft's SemanticKernel is, what the advantages of using it are, and provide a simple example of…]]></description><link>https://nearform.hashnode.dev/how-to-create-a-genai-agent-using-semantic-kernel</link><guid isPermaLink="true">https://nearform.hashnode.dev/how-to-create-a-genai-agent-using-semantic-kernel</guid><category><![CDATA[genai]]></category><category><![CDATA[Kernel]]></category><category><![CDATA[ai-agent]]></category><category><![CDATA[financial data]]></category><category><![CDATA[semantic kernel]]></category><dc:creator><![CDATA[Nearform]]></dc:creator><pubDate>Fri, 09 Aug 2024 13:59:26 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1723211716649/76d052a5-a7b5-4f00-861f-8d2794b0041a.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>By Marcelo Ferreira</p>
<h2 id="heading-our-example-sets-an-ai-agent-the-aim-of-retrieving-calculating-and-plotting-financial-data">Our example sets an AI agent the aim of retrieving, calculating and plotting financial data</h2>
<p>In this article, we explore what <a target="_blank" href="https://github.com/microsoft/semantic-kernel">Microsoft's SemanticKernel</a> is, what the advantages of using it are, and provide a simple example of how to use Semantic Kernel to create an AI agent to accomplish the task of displaying different value graphs for a stock in a specific time period.</p>
<p>This provides a reasonably complex example that uses external data sources and custom data formatting.</p>
<h2 id="heading-what-is-semantic-kernel">What is Semantic Kernel?</h2>
<p>Microsoft’s Semantic Kernel is a software development kit (SDK) that integrates Large Language Models (LLMs) with programming languages like C#, Python, and Java. The SDK does this by allowing us to define programming functions as plugins that can be easily chained together in a few lines of code.</p>
<p>Another major feature of Semantic Kernel is its ability to automatically orchestrate (or coordinate) plugins using AI. Simply provide the Semantic Kernel with an LLM and the plugins you need, then create a planner. When you ask the planner to achieve a specific goal, the Semantic Kernel will execute the plan for you.</p>
<h2 id="heading-what-is-an-ai-agent">What is an AI agent?</h2>
<p>An AI agent is a software program designed to perform tasks autonomously to achieve predetermined goals. It interacts with its environment (such as code functions, APIs, and data sources) and independently determines the best tools and actions to use for each task. AI agents can adapt to changes in their environment, learn from experiences, and improve their performance over time.</p>
<p>In summary, the user provides the goal which the AI should achieve and the AI will determine and execute the best approach to achieving that result.</p>
<h2 id="heading-ai-agent-financial-stock-plotter">AI agent: Financial stock plotter</h2>
<p>We utilised Semantic Kernel to create an AI agent. The aim of the agent is to retrieve financial data, calculate the drawdown of the data, and finally plot both the data and the drawdown.</p>
<p><img src="https://www.nearform.com/media/pages/digital-community/how-to-create-a-genai-agent-using-semantic-kernel/6d134ac978-1723128599/how-to-create-a-genai-agent-using-semantic-kernel-semantic-20kernel-20ai-20agent-1600x.png" alt="GenAI agent finance tracker" /></p>
<p>The above diagram shows how the agent communicates with the required APIs and functions to perform the desired tasks. A step-by-step explanation is available below:</p>
<p>1. The user asks a question via the terminal: <code>User: give me details on Apple over the last 6 months</code></p>
<p>2. The agent will process the query to retrieve the required fields from the user’s query:</p>
<p>a. <code>ticker_name: AAPL</code></p>
<p>b. <code>period: 6mo</code></p>
<p>3. The agent will call a function to retrieve the historical data from the Yahoo finance API. This data includes a variety of information on the stock over the period provided. The values we are interested in are: <code>date</code>and <code>close</code></p>
<p>a. <strong>date:</strong> the day the stock values were at the level indicated</p>
<p>b. <strong>close</strong>: the value of the stock at the time of closing of the financial markets on that day</p>
<p>4. The agent will call a function to calculate the drawdown</p>
<p>a. The <a target="_blank" href="https://en.wikipedia.org/wiki/Drawdown_(economics)">drawdown</a> is a financial measure that looks at the price of a stock at a certain point in time, gets the previous max value the stock held, and subtracts the max from the current value. The drawdown is a negative score as the current value is never greater than the cumulative max value up to the date we are looking at. The closer the drawdown is to zero, the closer the stock is to its maximum value.</p>
<p>5. The agent will then generate dynamic Python code to plot the data in a valid format using <a target="_blank" href="https://pandas.pydata.org/">pandas</a> and <a target="_blank" href="https://matplotlib.org/">matplotlib</a>.</p>
<p>6. Finally, the agent will refactor the code to attach the data calculated earlier on to the code just generated, then execute it</p>
<p><img src="https://www.nearform.com/media/pages/digital-community/how-to-create-a-genai-agent-using-semantic-kernel/7c61f61546-1723128815/how-to-create-a-genai-agent-using-semantic-kernel-aapl-1600x.png" alt="The plotted equity data (closing value) and drawdown data" /></p>
<p><em>The plotted equity data (closing value) and drawdown data</em></p>
<h2 id="heading-creating-an-agent">Creating an agent</h2>
<p>At the time of writing, Semantic Kernel allows four types of agents, called planners.</p>
<h3 id="heading-sequential-planner">Sequential planner</h3>
<p>Sequential planner takes a goal (prompt) and runs through the plugins provided to the agent at creation time, in the order requested by the goal. The loop will continue until the goal is complete. This is the agent we used to accomplish our goal.</p>
<p>The process our planner follows is shown below:</p>
<p><img src="https://www.nearform.com/media/pages/digital-community/how-to-create-a-genai-agent-using-semantic-kernel/96ab5ffe71-1723128971/how-to-create-a-genai-agent-using-semantic-kernel-user-query-1600x.png" alt="User query" /></p>
<h3 id="heading-stepwise-planner">Stepwise planner</h3>
<p>An agent that will run through the user request and process it step by step. It differs from the sequential planner by only running a specific agent against a part of the query. This allows the agent to break down the query into individual parts and run those against the plugin.</p>
<p>An example is if we asked “What would be the temperature in New York if we added 10 degrees to the current temperature?” the stepwise planner will be able to split the weather API call and the math problem from the query and run them separately, giving answers based on them.</p>
<h3 id="heading-handlebar-planner">Handlebar planner</h3>
<p>The main advantage of the handlebar planner over the stepwise planner is the usage of the handlebar templating language to generate the plan. This is due to the majority of LLMs already supporting the handlebar interface when dealing with prompts, which helps provide improve the accuracy of the response (in this case the agent plan). In opposition to the stepwise planner that uses XML when generating the plan. Using this language we can also provide loops and conditions that otherwise would only be available in coding languages.</p>
<p>Another advantage of this planner is the fact that the return value of the serialised function <code>planner.getPlan</code> is a readable text file that can be stored and reloaded at a later date. However, this planner is not available in the Python version of the kernel, as of the time of writing.</p>
<h3 id="heading-custom-planner">Custom planner</h3>
<p>Semantic Kernel offers us the ability to create our agent with custom rules. This involves more complexity but also gives more freedom to the developer to create the agent in the way they intend. It is particularly useful when you want human intervention during the agent planning stages, for example, to provide new variables or access custom data sources.</p>
<p>At the time of writing, the team behind Semantic Kernel intends to deprecate the Handlebar and Stepwise planners, in favour of this approach.</p>
<h3 id="heading-our-agent">Our agent</h3>
<p>The below code demonstrates how we created our agent using the Sequential planner:</p>
<pre><code class="lang-python"><span class="hljs-keyword">from</span> semantic_kernel <span class="hljs-keyword">import</span> Kernel
<span class="hljs-keyword">from</span> semantic_kernel.connectors.ai.open_ai <span class="hljs-keyword">import</span> OpenAIChatCompletion, OpenAIChatPromptExecutionSettings
<span class="hljs-keyword">from</span> semantic_kernel.functions <span class="hljs-keyword">import</span> KernelArguments
<span class="hljs-keyword">from</span> semantic_kernel.connectors.ai.function_call_behavior <span class="hljs-keyword">import</span> FunctionCallBehavior
<span class="hljs-keyword">from</span> plugins <span class="hljs-keyword">import</span> CodeRefactor, FinancePlugi
<span class="hljs-keyword">from</span> semantic_kernel.planners <span class="hljs-keyword">import</span> SequentialPlanner

<span class="hljs-comment"># Create the Kernel Instance</span>
kernel = Kernel()

execution_settings = OpenAIChatPromptExecutionSettings(
    service_id=<span class="hljs-string">"chat"</span>
)

<span class="hljs-comment"># Configure AI service used by the kernel</span>
kernel.add_service(
    OpenAIChatCompletion(service_id=service_id,
    api_key=open_ai_key,
    ai_model_id=ai_model_id),
)

<span class="hljs-comment"># Adding our custom plugins to the kernel</span>
kernel.add_plugin(parent_directory=<span class="hljs-string">"./plugins"</span>, plugin_name=<span class="hljs-string">"ChatPlugin"</span>)
kernel.add_plugin(plugin=FinancePlugin(), plugin_name=<span class="hljs-string">"FinancePlugin"</span>)
kernel.add_plugin(parent_directory=<span class="hljs-string">"./plugins"</span>, plugin_name=<span class="hljs-string">"ChartPlugin"</span>)
kernel.add_plugin(plugin=CodeRefactor(), plugin_name=<span class="hljs-string">"CodePlugin"</span>)

<span class="hljs-comment"># Define the details to be passed to the planner</span>
arguments = KernelArguments(settings=execution_settings)
arguments[<span class="hljs-string">"user_input"</span>]= <span class="hljs-string">"Show me the drawdown for Apple stock"</span>

goal = <span class="hljs-string">'''Based on the user_input argument,chat with the AI to get
                    the ticker_name and period if available,
           extract the data for a ticker over time,
           create a drawdown, plot the chart, 
           refactor the code to include the drawdown data.
       '''</span>

<span class="hljs-comment"># Create the plan</span>
plan = <span class="hljs-keyword">await</span> planner.create_plan(goal = goal)

<span class="hljs-comment"># Execute the plan</span>
result = <span class="hljs-keyword">await</span> plan.invoke(kernel=kernel, arguments=arguments)
</code></pre>
<h3 id="heading-steps-to-create-an-ai-agent">Steps to create an AI agent</h3>
<ol>
<li><p><strong>Create the kernel Instance</strong>: This is the starting point to map our planner to the plugins and LLM. We can generate more than one planner with the same kernel, allowing for parallel agents if needed.</p>
</li>
<li><p><strong>Configure AI service used by the kernel</strong>: This allows the tool to interpret user input into the right parameters and ensure the right tool is called at the right time to accomplish the goal.</p>
</li>
<li><p><strong>Adding our custom plugins to the kernel</strong>: This enables non-standard LLM calls to be usable by our agent</p>
</li>
<li><p><strong>Define the details to be passed to the planner</strong>: These include standard LLM settings (temperature, token limit, …) and function handling instructions.</p>
</li>
<li><p><strong>Create the Plan</strong>: Define the “System Message” for the planner as the goal. This instructs the LLM about the behaviour we expect our agent to have. The goal should be straightforward and use similar terminology available in the tools' descriptions.</p>
</li>
<li><p><strong>Execute the plan:</strong> Connects the details defined in step 4 to the plan and triggers the invocation step for the plan to start.</p>
</li>
</ol>
<h2 id="heading-plugins">Plugins</h2>
<p>A Semantic Kernel plugin is a software component designed to extend the LLM's functionality by either granting the LLM capabilities that it did not previously have (code plugins) or defining the purpose of the LLM invocation to a specific topic (prompt plugins).</p>
<p>In contrast to tools supported natively by the LLM (such as function invocation), a plugin is not attached directly to the model but is placed within the planner. The planner will then generate a step-by-step approach and see if any of the steps of the goal are associated with the plugin and will call the function in question.</p>
<p>The main drawback of plugins is the requirement for a string response from the functions, as structured data is easier to manipulate in code and we don’t have to use resources ‘stringifying’ and parsing all our content. This is due to Semantic Kernel not being aware of the planner's plan before it is in action, making it so that all responses need to be compatible with an LLM call, which defaults to text.</p>
<h3 id="heading-code-plugins">Code plugins</h3>
<p>Code plugins expand functionality by granting the LLM new “skills” it did not have access to prior.</p>
<p>The function is a standard Python function. To transform this to a code input we added the function decorator called <code>kernel_function</code><strong>.</strong> This decorator expects 2 arguments:</p>
<ul>
<li><p><strong>name,</strong> which is the name of the function to be referenced when attaching the plugin.</p>
</li>
<li><p><strong>description,</strong> which is a string value that indicates what the intention of the function is. This helps the planner identify when it needs to call this function and when it should avoid it.</p>
</li>
</ul>
<p>The two main use cases for code plugins are the data based on the developer’s intention, or accessing external APIs. We demonstrate both abilities below:</p>
<p><strong>Accessing an external API</strong></p>
<pre><code class="lang-python"><span class="hljs-keyword">import</span> yfinance <span class="hljs-keyword">as</span> yf
<span class="hljs-keyword">import</span> pandas <span class="hljs-keyword">as</span> pd
<span class="hljs-keyword">from</span> semantic_kernel.functions.kernel_function_decorator <span class="hljs-keyword">import</span> kernel_function

<span class="hljs-comment"># defines this function as a plugin function</span>
<span class="hljs-meta">@kernel_function(</span>
  name=<span class="hljs-string">"financial_info"</span>,
  description=<span class="hljs-string">"gets the historic details for a stock"</span>)
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">financial_info</span>(<span class="hljs-params">
  self,
  TICKER_AND_PERIOD</span>):</span>

  <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> TICKER_AND_PERIOD:
     <span class="hljs-keyword">raise</span> Exception(<span class="hljs-string">"No ticker value provided"</span>)

  args = json.loads(TICKER_AND_PERIOD)
  ticker_name =  args[<span class="hljs-string">"ticker_name"</span>]
  period =  args[<span class="hljs-string">"period"</span>]

  <span class="hljs-keyword">if</span> ticker_name <span class="hljs-keyword">is</span> <span class="hljs-keyword">not</span> <span class="hljs-literal">None</span>:    
     ticker = yf.Ticker(ticker_name)
     time = period <span class="hljs-keyword">if</span> period <span class="hljs-keyword">is</span> <span class="hljs-keyword">not</span> <span class="hljs-literal">None</span> <span class="hljs-keyword">else</span> <span class="hljs-string">'1y'</span>
     data = ticker.history(period = time)
     data.index = data.index.strftime(<span class="hljs-string">'%Y-%m-%d'</span>)
     data[<span class="hljs-string">"Close"</span>] = data[<span class="hljs-string">"Close"</span>].round(<span class="hljs-number">4</span>)

     print(<span class="hljs-string">'data retrieval complete'</span>)

     <span class="hljs-keyword">return</span> data.to_json()
</code></pre>
<p>As shown in the code snippet above, we want to retrieve the historical data for a specific ticker using the Yahoo Finance API.</p>
<p>We created the base function for it when we instantiate a Ticker based on the input. Notice that the model is able to convert the string input to the required variables, before calling the plugins.</p>
<p>Then, we use the historical data to get the closing prices for the period specified. Finally, we manipulate the pandas DataFrame to format the style of the columns <code>Close</code> and the <code>date</code> index as these will be used in future steps of the process.</p>
<p><strong>Processing data</strong></p>
<pre><code class="lang-python"><span class="hljs-keyword">import</span> yfinance <span class="hljs-keyword">as</span> yf
<span class="hljs-keyword">import</span> pandas <span class="hljs-keyword">as</span> pd
<span class="hljs-keyword">from</span> semantic_kernel.functions.kernel_function_decorator <span class="hljs-keyword">import</span> kernel_function

<span class="hljs-meta">@kernel_function(name="drawdown", description="Calculate the drawdown values for the stock")</span>
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">drawdown</span>(<span class="hljs-params">self, data_json: Annotated[str, <span class="hljs-string">"the data from the stock"</span>]</span>):</span>
   data =  pd.read_json(StringIO(data_json))

   data[<span class="hljs-string">'Peak'</span>] = data[<span class="hljs-string">'Close'</span>].cummax()
   data[<span class="hljs-string">'Drawdown'</span>] = data[<span class="hljs-string">'Close'</span>] - data[<span class="hljs-string">'Peak'</span>]
   data = data.drop(columns=[<span class="hljs-string">"Peak"</span>])

   print(<span class="hljs-string">'drawdown calculated'</span>)

   <span class="hljs-keyword">return</span> str(data.to_json(orient=<span class="hljs-string">'split'</span>))
</code></pre>
<p>We want to calculate the drawdown for the data based on the formula: <code>drawdown = current - cummax</code>. Pandas provides us with the functionality required to perform this calculation.</p>
<p>Our original data looked like this:</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Date</td><td>Close</td></tr>
</thead>
<tbody>
<tr>
<td>2024-01-01</td><td>102.01</td></tr>
<tr>
<td>2024-01-02</td><td>101.02</td></tr>
</tbody>
</table>
</div><p>After the function run our data looks like this:</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Date</td><td>Close</td><td>Drawdown</td></tr>
</thead>
<tbody>
<tr>
<td>2024-01-01</td><td>102.02</td><td>0</td></tr>
<tr>
<td>2024-01-02</td><td>101.01</td><td>-1.01</td></tr>
</tbody>
</table>
</div><p>Now that we have our data available, we will be moving to a prompt plugin to generate dynamic code.</p>
<h3 id="heading-prompt-plugins">Prompt plugins</h3>
<p>Prompt plugins are used to alter the original intention of the LLM being invoked by providing it with new instructions. The plugin files are a prompt file and a JSON file. The prompt is the instructions that the LLM must abide by. The JSON file contains the LLM values to take into account.</p>
<p>The content of these files looks like this:</p>
<pre><code class="lang-markdown">You are a chatbox that will retrieve the ticker<span class="hljs-emphasis">_name and the period
(if provided) for the company in input in a json format

Example:
    query: Give me details on Apple over the last year
    result: ticker_</span>name = NVDA, period= 1y

<span class="hljs-code">    query: Give me details on Nvidia
    result: ticker_name = NVDA, period= null
</span>
<span class="hljs-code">    query: What colour is the sky
    result: No ticker available
</span>
input={{$user<span class="hljs-emphasis">_input}}</span>
</code></pre>
<pre><code class="lang-json">{
  <span class="hljs-attr">"schema"</span>: <span class="hljs-number">1</span>,
  <span class="hljs-attr">"description"</span>: <span class="hljs-string">"From the data generate code that will map the data over a time period"</span>,
  <span class="hljs-attr">"execution_settings"</span>: {
    <span class="hljs-attr">"default"</span>: {
      <span class="hljs-attr">"max_tokens"</span>:<span class="hljs-number">4096</span>,
      <span class="hljs-attr">"temperature"</span>: <span class="hljs-number">0.1</span>,
      <span class="hljs-attr">"stop_sequences"</span>: [
        <span class="hljs-string">"[Done]"</span>
      ]
    }
  }
}
</code></pre>
<p>The keyword filenames <code>skprompt</code> and <code>config</code> will let the planner know that these are prompt plugins. The prompt file will contain a straight prompt instruction similar to an LLM Model system message, which provides instructions to the model.</p>
<p>The config file will determine the <code>kwargs</code> information for the prompt. This is the equivalent of writing the below code but with the correct mappings, and the input required based on the part of the goal this step is aiming to achieve:</p>
<pre><code class="lang-python">f = open(<span class="hljs-string">'skprompt.txt'</span>, <span class="hljs-string">'r'</span>)
response = client.chat.completions.create(
  model=current_model,
  messages=[
     {<span class="hljs-string">"role"</span>: <span class="hljs-string">"system"</span>, <span class="hljs-string">"content"</span>: f.read()},
     {<span class="hljs-string">"role"</span>: <span class="hljs-string">"user"</span>, <span class="hljs-string">"content"</span>: <span class="hljs-string">"user_input"</span>: user_input}
  ],
  temperature=<span class="hljs-number">0.1</span>,
  max_tokens=<span class="hljs-number">4096</span>,
  stop=stop_sequence,
)
</code></pre>
<p>The plugins are then attached to the kernel instance before we instantiate the planner. Hence, any planners we may have will benefit from the plugins provided.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Various tools are available to create GenAI agents for specific tasks, each with its strengths and weaknesses. Semantic Kernel is an interesting tool for creating straightforward agents that perform relatively well for simple tasks.</p>
<p>As illustrated above, we can use Semantic Kernel to quickly create AI agents that can retrieve and modify data as well as perform LLM requests to accomplish a predetermined goal. Semantic Kernel is capable of utilising the LLM to create the best approach to a task and which plugins to consume to ensure the task is completed.</p>
<p>The two main advantages explored in this article are Plugins and Planners. Plugins are versatile and powerful and require minimal alteration to functions for code plugins, and standard text values for prompt plugins. The variety of the planners and the ease of creation makes them very reliable for different types of agents.</p>
]]></content:encoded></item><item><title><![CDATA[Streamline your mobile testing with Maestro]]></title><description><![CDATA[By Ludovico Besana In the ever-evolving landscape of mobile app development, efficient and effective testing is crucial. Mobile testing ensures that…]]></description><link>https://nearform.hashnode.dev/streamline-your-mobile-testing-with-maestro</link><guid isPermaLink="true">https://nearform.hashnode.dev/streamline-your-mobile-testing-with-maestro</guid><category><![CDATA[mobile UI automation]]></category><category><![CDATA[maestro]]></category><category><![CDATA[Mobile Testing Automation ]]></category><category><![CDATA[mobile ui]]></category><category><![CDATA[mobile testing,]]></category><dc:creator><![CDATA[Nearform]]></dc:creator><pubDate>Fri, 02 Aug 2024 13:23:11 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1722603269260/e0596453-a7f7-4ed4-bb22-a4cc14e3e9a7.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>By Ludovico Besana</p>
<h2 id="heading-comparing-maestro-with-other-popular-tools-for-mobile-ui-automation">Comparing Maestro with other popular tools for mobile UI automation</h2>
<p>In the ever-evolving landscape of mobile app development, efficient and effective testing is crucial. Mobile testing ensures that applications work seamlessly across different devices and platforms. By choosing the right tools, teams can significantly enhance their productivity and the quality of their applications.</p>
<p>This article explores the features of <a target="_blank" href="https://maestro.mobile.dev/">Maestro</a>, a promising tool for mobile UI automation, and compares it with other popular tools such as <a target="_blank" href="https://docs.robotframework.org/docs/different_libraries/appium">Robot Framework Appium Library</a> and <a target="_blank" href="https://webdriver.io/">WebdriverIO</a>.</p>
<h2 id="heading-real-world-application-of-maestro">Real-world application of Maestro</h2>
<p>A common challenge in mobile UI testing is managing the complexity of test setups and ensuring consistent results across different environments. Maestro offers a solution by enabling users to perform tests using a simple YAML syntax, which can be easily understood and written by anyone in the organisation. This flexibility allows teams to quickly adapt to changes and make reliable decisions promptly.</p>
<h3 id="heading-a-simple-maestro-test-case">A simple Maestro test case</h3>
<p>Here is an example of how a Maestro test case is structured using YAML:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">appId:</span> <span class="hljs-string">org.wikipedia</span>
<span class="hljs-meta">---</span>
<span class="hljs-bullet">-</span> <span class="hljs-attr">tapOn:</span> <span class="hljs-string">"ADD OR EDIT.*"</span>
<span class="hljs-bullet">-</span> <span class="hljs-attr">tapOn:</span> <span class="hljs-string">"ADD LANGUAGE"</span>
<span class="hljs-bullet">-</span> <span class="hljs-attr">tapOn:</span>
    <span class="hljs-attr">id:</span> <span class="hljs-string">".*menu_search_language"</span>
<span class="hljs-bullet">-</span> <span class="hljs-attr">inputText:</span> <span class="hljs-string">"French"</span>
<span class="hljs-bullet">-</span> <span class="hljs-attr">assertVisible:</span> <span class="hljs-string">"Français"</span>
<span class="hljs-bullet">-</span> <span class="hljs-attr">tapOn:</span> <span class="hljs-string">"Français"</span>
<span class="hljs-bullet">-</span> <span class="hljs-attr">tapOn:</span> <span class="hljs-string">"Back"</span>
</code></pre>
<p>This example demonstrates a simple yet comprehensive test case for a hypothetical Wikipedia application:</p>
<p><strong>1. appId</strong>: Specifies the ID of the application to be tested.</p>
<p><strong>2. tapOn: "ADD OR EDIT.*"</strong>: Simulates a tap on any element whose text matches the pattern "ADD OR EDIT".</p>
<p><strong>3. tapOn: "ADD LANGUAGE"</strong>: Simulates a tap on the "ADD LANGUAGE" button.</p>
<p><strong>4. tapOn (with id)</strong>: Simulates a tap on an element whose ID matches the pattern "menu_search_language".</p>
<p><strong>5. inputText: "French"</strong>: Inputs the text "French" into the input field.</p>
<p><strong>6. assertVisible: "Français"</strong>: Verifies that the text "Français" is visible on the screen.</p>
<p><strong>7. tapOn: "Français"</strong>: Simulates a tap on the "Français" option.</p>
<p><strong>8. tapOn: "Back"</strong>: Simulates a tap on the "Back" button to return to the previous screen.</p>
<h3 id="heading-localised-testing-challenges">Localised testing challenges</h3>
<p>Running tests in different languages can introduce complications, and this is true for any testing approach. With Maestro, this issue arises when the simulator's language settings do not match the language expected by the test scripts. For instance, if your simulator is set to Italian and your tests are designed for an English interface, the tests will fail because the locators (which depend on text strings) will not match.</p>
<p><img src="https://www.nearform.com/media/pages/digital-community/streamline-your-mobile-testing-with-maestro/e053ce542b-1722603174/blog-streamline-your-mobile-testing-with-maestro-a-running-test-with-maestro-1600x.png" alt="A running test with Maestro" /></p>
<p><em>A running test with Maestro</em></p>
<p>To address this, Maestro provides the ability to set up and configure simulators for different locales. Here are some solutions:</p>
<ul>
<li><strong>Locale configuration</strong>: Ensure that the simulator's locale matches the language of your test scripts. You can configure the locale using Maestro's command-line options:</li>
</ul>
<pre><code class="lang-text">maestro start-device --platform ios --device-locale en_US --os-version=15
</code></pre>
<ul>
<li><p><strong>Use of IDs and universal locators</strong>: Instead of relying solely on text strings, use element IDs and universal locators that remain consistent across different languages.</p>
</li>
<li><p><strong>Parameterised tests</strong>: Create parameterised test scripts that can adapt to different locales by using variables for text strings, making it easier to run the same tests in multiple languages.</p>
</li>
</ul>
<p>By implementing these strategies, you can minimise discrepancies and ensure more reliable test results across various language settings with Maestro.</p>
<h3 id="heading-emulator-setup">Emulator setup</h3>
<p>Setting up emulators for different locales and operating system versions can be a challenge with Maestro. For instance, initiating a test run on an iOS simulator in British English requires specific commands and configurations:</p>
<pre><code class="lang-text">maestro start-device --platform ios --device-locale en_GB --os-version=17
</code></pre>
<p>It’s important to note that Maestro supports specific OS versions, such as iOS 15, 16, and 17, at the time of writing. For the most current list of supported versions, refer to the <a target="_blank" href="https://cloud.mobile.dev/reference/device-configuration#simulator-specs">Maestro documentation</a>. Setting up the appropriate emulator can be tricky, especially for those not familiar with mobile testing environments.</p>
<h3 id="heading-flaky-tests-and-element-inspection">Flaky tests and element inspection</h3>
<p>A critical aspect of mobile testing is the accurate inspection of elements within the application. <a target="_blank" href="https://maestro.mobile.dev/getting-started/maestro-studio">Maestro Studio</a> is a tool within Maestro that helps testers design and execute tests through a visual interface. However, relying solely on Maestro Studio may not always allow for precise element inspection, leading to flaky tests if elements are not correctly identified.</p>
<p>Element IDs are unique identifiers for UI elements within an app, similar to how IDs are used in web applications. These IDs are crucial for ensuring tests interact with the correct elements. Without access to the app’s codebase, it can be challenging to inspect and identify these elements accurately.</p>
<p>To address this, tools like <a target="_blank" href="https://github.com/appium/appium-inspector">Appium Inspector</a> can be used alongside Maestro. Appium Inspector provides a comprehensive solution for inspecting app elements, allowing testers to accurately identify and interact with elements even without direct access to the codebase. A common practice is to use such inspectors to identify elements and reference the app’s codebase when possible to retrieve IDs.</p>
<p>By combining these tools, testers can ensure more reliable and accurate test results, reducing the likelihood of flaky tests.</p>
<p><img src="https://www.nearform.com/media/pages/digital-community/streamline-your-mobile-testing-with-maestro/d7aedeb72d-1722603174/blog-streamline-your-mobile-testing-with-maestro-maestro-studio-1600x.png" alt="Maestro Studio Locators Inspector" /></p>
<p><em>Maestro Studio Locators Inspector</em></p>
<h3 id="heading-ai-integration-in-maestro">AI integration in Maestro</h3>
<p>One of Maestro’s main features is its integration of artificial intelligence (AI), which assists users in various ways:</p>
<ul>
<li><p><strong>Automated element identification</strong>: The AI helps automatically identify UI elements, reducing the time needed to write test cases.</p>
</li>
<li><p><strong>Intelligent test recommendations</strong>: Based on the application’s structure and previous test cases, Maestro's AI can suggest test steps and improvements, ensuring more comprehensive test coverage.</p>
</li>
</ul>
<p>Thanks to these AI capabilities, Maestro not only reduces setup time but also enhances the reliability of tests. Teams can then focus more on the core functionalities of their applications, ensuring higher quality and a faster time-to-market. This integration of AI into the testing process is relatively new, making Maestro an innovative tool in the mobile testing landscape.</p>
<p><img src="https://www.nearform.com/media/pages/digital-community/streamline-your-mobile-testing-with-maestro/4d6f6d102b-1722603175/blog-streamline-your-mobile-testing-with-maestro-maestro-studio-1600x.gif" alt="Maestro Studio AI Feature" /></p>
<p><em>Maestro Studio AI Feature</em></p>
<h3 id="heading-parallel-test-execution">Parallel test execution</h3>
<p>While Maestro simplifies many aspects of mobile testing, it does not support running tests in parallel across different simulators directly. Even with two simulators and two terminals open, parallel execution is not possible. There is, however, the option to run tests in the cloud within a mobile development environment, although this incurs additional costs.</p>
<h2 id="heading-comparing-mobile-testing-tools">Comparing mobile testing tools</h2>
<p>Choosing the right tool for mobile test automation can significantly impact efficiency, accessibility, and test maintenance. Here, we compare three main tools: Maestro, Robot Framework Appium Library, and WebdriverIO.</p>
<h3 id="heading-maestro">Maestro</h3>
<p>Maestro is designed for simplicity and ease of use, making it ideal for teams with limited coding experience. Its YAML-based syntax allows for quick test creation and execution, which is particularly useful for small projects or teams new to automation. The integration of AI in Maestro Studio provides intelligent test recommendations and automated element identification, enhancing productivity and test reliability.</p>
<h3 id="heading-robot-framework-appium-library">Robot Framework Appium Library</h3>
<p>Robot Framework Appium Library leverages the power of Python and the flexibility of Robot Framework. It is suited for teams with a solid coding background who need a robust and versatile testing solution. This tool offers comprehensive support for various mobile testing scenarios but requires more setup and a steeper learning curve compared to Maestro.</p>
<h3 id="heading-webdriverio">WebdriverIO</h3>
<p>WebdriverIO is a JavaScript-based framework that supports both web and mobile testing. It is highly extensible and integrates well with various CI/CD pipelines, making it suitable for teams already familiar with JavaScript. WebdriverIO strikes a balance between usability and flexibility, offering powerful features for both beginner and advanced testers.</p>
<h3 id="heading-feature-comparison-table">Feature comparison table</h3>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Feature</td><td>Maestro</td><td>Robot Framework Appium Library</td><td>WebdriverIO</td></tr>
</thead>
<tbody>
<tr>
<td>Syntax</td><td>YAML</td><td>Python (Robot Framework)</td><td>JavaScript</td></tr>
<tr>
<td>Platform support</td><td>Android, iOS, React Native, Flutter, Web Views, .NET MAUI</td><td>Android, iOS</td><td>Android, iOS, Web</td></tr>
<tr>
<td>Setup complexity</td><td>Low</td><td>Medium</td><td>Medium</td></tr>
<tr>
<td>Execution speed</td><td>Fast</td><td>Slow</td><td>Medium</td></tr>
<tr>
<td>Accessibility</td><td>High (no coding required)</td><td>Medium</td><td>High (JavaScript friendly)</td></tr>
<tr>
<td>Test management</td><td>Simple (YAML flows)</td><td>Complex</td><td>Medium</td></tr>
</tbody>
</table>
</div><h3 id="heading-when-to-use-each-tool">When to use each tool</h3>
<ul>
<li><p><strong>Maestro</strong>: Recommended for teams seeking quick setup and ease of use, especially when team members have limited coding knowledge. Ideal for small to medium projects where speed and simplicity are crucial.</p>
</li>
<li><p><strong>Robot Framework Appium Library</strong>: Best for teams with strong coding skills who require a highly versatile and robust testing solution. Suitable for complex projects that demand extensive customisation.</p>
</li>
<li><p><strong>WebdriverIO</strong>: Great for teams with JavaScript expertise looking for a flexible tool that can handle both web and mobile testing. Perfect for projects that benefit from extensive integrations and a balance of ease of use and power.</p>
</li>
</ul>
<p>By understanding the strengths and appropriate use cases for each tool, teams can make informed decisions to enhance their mobile testing strategy.</p>
<h2 id="heading-final-considerations">Final considerations</h2>
<p>Based on our experience, we consider Maestro an exceptionally valuable tool for those new to mobile UI automation. Its simplicity in setup and use makes it particularly suitable for both small projects and those looking to gain a basic understanding of UI automation without facing a steep learning curve. However, for more complex projects, it might be necessary to opt for a more powerful and versatile tool.</p>
<p>Maestro serves as a precious resource for organisations aiming to improve the quality of their mobile applications through efficient and manageable testing processes. By reducing the complexity and time required to configure and execute tests, Maestro enables teams to focus more on innovation, reducing time to market and enhancing the quality of the final products.</p>
]]></content:encoded></item><item><title><![CDATA[An introduction to cross-cloud access in managed Kubernetes clusters]]></title><description><![CDATA[By Jeevanandham Poongavanam In the ever-evolving landscape of cloud computing, it's increasingly common to face scenarios that necessitate cross-cloud…]]></description><link>https://nearform.hashnode.dev/an-introduction-to-cross-cloud-access-in-managed-kubernetes-clusters</link><guid isPermaLink="true">https://nearform.hashnode.dev/an-introduction-to-cross-cloud-access-in-managed-kubernetes-clusters</guid><category><![CDATA[General Programming]]></category><category><![CDATA[Programming Blogs]]></category><category><![CDATA[iwritecode]]></category><category><![CDATA[Kubernetes]]></category><category><![CDATA[Cloud]]></category><dc:creator><![CDATA[Nearform]]></dc:creator><pubDate>Fri, 26 Jul 2024 10:19:45 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1721988739697/72a867fe-8ab2-4cca-bfff-9153e2d0b70a.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>By Jeevanandham Poongavanam</p>
<h2 id="heading-the-insights-we-gained-from-implementing-cross-cloud-access">The insights we gained from implementing cross-cloud access</h2>
<p>In the ever-evolving landscape of cloud computing, it's increasingly common to face scenarios that necessitate cross-cloud access. Whether it’s accessing AWS services from an Azure Kubernetes Service (AKS) cluster or leveraging Azure resources from an AWS Elastic Kubernetes Service (EKS) cluster, these scenarios present unique challenges that demand innovative solutions.</p>
<h2 id="heading-a-project-where-we-migrated-to-aks-background-and-challenges">A project where we migrated to AKS: Background and challenges</h2>
<p>In one of my recent projects, we encountered the need to migrate an application from a self-managed Kubernetes cluster hosted on AWS to Azure Kubernetes Services. This transition was not just a simple lift-and-shift, as it also involved enabling secure access to cloud resources across different cloud providers.</p>
<p>Official documentation often falls short in covering these complex scenarios comprehensively, leading to potential gaps in implementation. Through this blog post, I aim to share insights gained from implementing cross-cloud access, focusing on the integration between Azure and AWS. This discussion will cover two key scenarios:</p>
<ol>
<li><p>Enabling access to AWS services from an Azure AKS cluster.</p>
</li>
<li><p>Facilitating access to Azure services from an AWS EKS cluster.</p>
</li>
</ol>
<p>Our goal is to foster a harmonious interaction between these two cloud giants, overcoming the common obstacles encountered in such integrations and securely granting access without the need for sharing credentials.</p>
<h2 id="heading-proposed-solution-aws-iam-role-for-service-account-irsa-and-azure-workload-identity-federation">Proposed solution: AWS IAM Role for Service Account (IRSA) and Azure workload identity federation</h2>
<p>Each cloud provider offers a bespoke solution to ensure secure access within their managed Kubernetes clusters. AWS employs the <a target="_blank" href="https://docs.aws.amazon.com/eks/latest/userguide/iam-roles-for-service-accounts.html">IRSA</a>, allowing workloads to safely assume roles that provide necessary permissions to access cloud resources. Concurrently, Azure has rolled out <a target="_blank" href="https://azure.github.io/azure-workload-identity/docs/">Azure Workload Identity</a> to streamline identity and access management. Both platforms require a specific add-on to inject tokens correlated with the service account.</p>
<p>Central to these mechanisms is OpenID Connect (OIDC), a robust and time-tested protocol foundational to identity verification across disparate platforms. OIDC is pivotal for securely interconnecting Azure and AWS services, offering a secure, scalable, and adaptable authentication framework that is crucial to our approach, guaranteeing smooth and dependable identity management across cloud environments.</p>
<h2 id="heading-openid-connect-primer">OpenID Connect primer</h2>
<p>OpenID Connect (OIDC) is a crucial protocol for verifying identities across various cloud platforms, enhancing the OAuth 2.0 framework by securely verifying a user's identity through an authorisation server and enabling access to resource servers. Although frequently used, the complexities of OAuth 2.0 and OIDC can initially be overwhelming, prompting a deeper exploration to understand how these protocols facilitate secure cross-cloud access.</p>
<h3 id="heading-understanding-oauth-20-and-oidc">Understanding OAuth 2.0 and OIDC</h3>
<p>In essence, accessing specific data or services requires authorisation to ensure the request is permitted and identity verification to confirm that the requestor is indeed who they claim to be. While OAuth 2.0 handles authorisation, it does not inherently confirm the requester's identity. OIDC addresses this gap by adding a layer of identity verification atop OAuth 2.0.</p>
<p><strong>Core components involved:</strong></p>
<ul>
<li><p><strong>User Data</strong>: The data or services needed, managed by a resource server.</p>
</li>
<li><p><strong>Resource Owner</strong>: The owner of the data or service.</p>
</li>
<li><p><strong>Authorisation Server</strong>: Authorises access requests.</p>
</li>
<li><p><strong>Relying Party/Client Application:</strong> Initiates access requests. Upon approval from the Resource Owner, the Authorisation Server issues an "Access Token."</p>
</li>
</ul>
<p>This framework ensures that data access is both authenticated and authorised.</p>
<h3 id="heading-oauth-20-flows">OAuth 2.0 Flows</h3>
<p>The OAuth 2.0 protocol includes various flows to accommodate different scenarios:</p>
<p><strong>1. Authorisation code flow</strong>: Used in systems like "Sign in with Google," where a user logs in to share specific data, receiving an access token in exchange for an authorisation code.</p>
<p><strong>2. Implicit flow:</strong> Previously popular in applications that couldn't securely store secrets, such as single-page applications. Now, due to security concerns, more secure methods like the Authorisation Code Flow with PKCE are recommended.</p>
<p><strong>3. Password credential flow</strong>: Employed when there is a strong trust relationship between the resource owner and the client, such as personal mobile apps.</p>
<p><strong>4. Client credential flow</strong>: Ideal for server-to-server interactions, where a client application uses its credentials to obtain an access token directly, granting access to the resource server.</p>
<p>In all these flows, OIDC enhances the process by adding an "ID Token" to verify the user's identity, providing a robust identity layer over the existing OAuth 2.0 authorisation structure. For further details, you can refer to the official <a target="_blank" href="https://openid.net/developers/specs/">OpenID Connect documentation</a>.</p>
<h2 id="heading-oidc-in-cloud-kubernetes-environments">OIDC in cloud Kubernetes environments</h2>
<p>In this section, I share detailed authentication steps for AWS IRSA with Amazon EKS and Azure Workload Identity in AKS.</p>
<h3 id="heading-aws-irsa-with-amazon-eks">AWS IRSA with Amazon EKS</h3>
<p>In AWS IRSA (IAM Roles for Service Accounts) within Amazon EKS, the OAuth flow used is akin to the <strong>Client Credentials Grant</strong>. This design is specifically tailored for machine-to-machine communication where an application acts on its own behalf rather than acting for a user. AWS IAM plays a critical role here, validating service account tokens issued by EKS and providing scoped AWS credentials for accessing AWS resources.</p>
<h4 id="heading-detailed-authentication-steps-in-aws-irsa-using-an-oidc-provider"><strong>Detailed authentication steps in AWS IRSA using an OIDC provider:</strong></h4>
<p><strong>1. OIDC provider configuration</strong>:</p>
<ul>
<li><p>Within AWS EKS, an OIDC provider is configured to connect the Kubernetes cluster's identity system with AWS IAM. This OIDC provider is not a separate service but a configuration within AWS IAM that trusts the Kubernetes cluster's service account tokens.</p>
</li>
<li><p>The OIDC provider in AWS is set up to trust tokens issued by the Kubernetes cluster, meaning it recognises these tokens as authentic credentials.</p>
</li>
</ul>
<p><strong>2. Service account token</strong>:</p>
<ul>
<li><p>Each Kubernetes pod can be assigned a specific service account. This account has a JWT (JSON Web Token) associated with it, which is automatically managed and rotated by EKS.</p>
</li>
<li><p>This JWT includes claims that identify the particular service account and permissions.</p>
</li>
</ul>
<p><strong>3. Authentication process</strong>:</p>
<ul>
<li><p>When a pod in EKS needs to access AWS resources, it retrieves its JWT from the file system (mounted by EKS into the pod).</p>
</li>
<li><p>The pod or the application inside it then presents this JWT in a request to AWS IAM as part of the API call to assume the linked IAM role.</p>
</li>
</ul>
<p><strong>4. Token validation</strong>:</p>
<ul>
<li><p>AWS IAM, acting as the OIDC provider, validates the JWT against the issuer URL, the audience (<code>aud</code>), and other claims included in the JWT. These validations ensure that the token is indeed issued by the trusted EKS cluster and that it is intended for the specific IAM role it is requesting.</p>
</li>
<li><p>This validation process is crucial as it confirms the authenticity and appropriateness of the request, effectively authenticating the service account's identity based on the OIDC standards.</p>
</li>
</ul>
<p><strong>5. Credential issuance</strong>:</p>
<ul>
<li>Upon successful validation, AWS IAM issues AWS credentials (access key, secret key, and session token) to the pod. These credentials are temporary and scoped to the permissions defined in the IAM role associated with the service account.</li>
</ul>
<p><strong>6. Using AWS resources</strong>:</p>
<ul>
<li>With these credentials, the pod can make authenticated and authorised API calls to AWS services, operating within the permissions boundaries set by the IAM role.</li>
</ul>
<p>For more information, refer to the official <a target="_blank" href="https://docs.aws.amazon.com/eks/latest/userguide/iam-roles-for-service-accounts.html">documentation</a>.</p>
<h3 id="heading-azure-workload-identity-in-aks">Azure Workload Identity in AKS</h3>
<p>Similarly, in Azure Kubernetes Service (AKS), the Azure Workload Identity utilises a flow comparable to the <strong>OAuth 2.0 Client Credentials Grant</strong>. This setup allows Kubernetes pods to securely access Azure resources by leveraging Azure Active Directory (AAD) identities. The AKS manages the association between Kubernetes service accounts and AAD identities, facilitating a secure and scalable authentication method without user interaction.</p>
<h4 id="heading-detailed-authentication-steps-in-azure-ad-workload-identity-for-kubernetes"><strong>Detailed authentication steps in Azure AD Workload Identity for Kubernetes:</strong></h4>
<p><strong>1. OIDC provider configuration:</strong></p>
<p>Azure AD (Active Directory) Workload Identity uses Azure Active Directory (AAD) as the OIDC provider. This configuration involves setting up Kubernetes to issue tokens that Azure AD can trust. The integration between Kubernetes and Azure AD allows Kubernetes service accounts to use these tokens to authenticate against Azure resources securely.</p>
<p><strong>2. Service account token:</strong></p>
<p>In Azure Kubernetes Service (AKS), each pod can use a Kubernetes service account that is automatically integrated with Azure AD using the Azure Workload Identity setup. The tokens issued to these service accounts are projected into the pod's file system and are used for authentication with Azure AD.</p>
<p><strong>3. Authentication process:</strong></p>
<ul>
<li><p>A pod needing to access Azure resources will use the token provided to its Kubernetes service account.</p>
</li>
<li><p>This token is presented to Azure AD as part of the request to access Azure resources.</p>
</li>
</ul>
<p><strong>4. Token validation:</strong></p>
<ul>
<li><p>Azure AD validates the Kubernetes service account token using established trust configurations. It checks the issuer, the audience, and other claims in the token to ensure it is valid and issued by a trusted Kubernetes cluster.</p>
</li>
<li><p>This validation is crucial as it ensures the token's authenticity and appropriateness for the requested Azure resource access.</p>
</li>
</ul>
<p><strong>5. Credential issuance:</strong></p>
<ul>
<li><p>Once the token is validated, Azure AD issues an Azure access token to the pod.</p>
</li>
<li><p>This token is specifically scoped to the permissions that are assigned to the Azure AD identity linked with the Kubernetes service account.</p>
</li>
</ul>
<p><strong>6. Using Azure resources:</strong></p>
<ul>
<li><p>The pod uses the Azure access token to make authenticated and authorised API calls to Azure services.</p>
</li>
<li><p>These services validate the Azure access token and provide access based on the permissions configured for the Azure AD identity.</p>
</li>
</ul>
<p>For more information, refer to the official <a target="_blank" href="https://azure.github.io/azure-workload-identity/docs/">documentation</a>.</p>
<h2 id="heading-running-irsa-in-aks">Running IRSA in AKS</h2>
<p>Following in the footsteps of IRSA in EKS, we simply need to substitute the EKS cluster with the AKS cluster. AKS hosts a public endpoint to expose the OpenID Configuration via the OIDC Issuer URL. Next, we need to create an IAM Identity Provider (IDP) that points to the AKS OIDC Issuer URL. This IDP is then associated with an IAM Role as a trust policy, along with a policy that grants the required permissions needed to access AWS services. Finally, we annotate the service account in the AKS cluster with the IAM Role ARN (Amazon Resource Name) that we created earlier.</p>
<p>In detail, we need to follow the below steps:</p>
<ol>
<li><p>In Azure: Create a resource group that hosts the AKS cluster.</p>
</li>
<li><p>In Azure: Create an AKS cluster and enable the OIDC Issuer.</p>
</li>
<li><p>In Azure: Create a service account (SA)</p>
</li>
<li><p>In Azure: Get the OIDC Issuer URL.</p>
</li>
<li><p>In AWS: Create a resource (an S3 bucket) to check the access.</p>
</li>
<li><p>In AWS: Create a role providing access to the S3 bucket created above.</p>
</li>
<li><p>In AWS: Create an identity provider that points to the AKS OIDC Issuer URL.</p>
</li>
<li><p>In Azure: Annotate the SA in AKS with the IAM Role ARN that we created above.</p>
</li>
<li><p>In Azure: Install the <code>amazon-eks-pod-identity</code> helm chart.</p>
</li>
<li><p>In Azure: Create a workload that uses the SA and try accessing the AWS Service in this case S3 bucket.</p>
</li>
</ol>
<p>In the next section, we’ll now get our hands dirty. I have used <code>azure cli</code> in the various steps you’re about to read — make sure that you have authenticated to Azure using your credentials.</p>
<p>At the end of the next section, our solution will look like this</p>
<p><img src="https://www.nearform.com/media/pages/digital-community/an-introduction-to-cross-cloud-access-in-managed-kubernetes-clusters/afa3031f16-1721726747/az-aws-1600x.png" alt="AWS solution" /></p>
<h3 id="heading-create-an-aks-cluster">Create an AKS cluster</h3>
<p>Run the below commands and note that we set some env variables which will build up over time and will be used later down the line.</p>
<pre><code class="lang-bash"><span class="hljs-comment"># Set required environment variables.</span>
<span class="hljs-built_in">export</span> LOCATION=<span class="hljs-string">"eastus"</span>
<span class="hljs-built_in">export</span> RESOURCE_GROUP_NAME=<span class="hljs-string">"rg-aks-az-identity"</span>
<span class="hljs-built_in">export</span> CLUSTER_NAME=<span class="hljs-string">"aks-az-identity"</span>
<span class="hljs-built_in">export</span> SERVICE_ACCOUNT_NAMESPACE=<span class="hljs-string">"default"</span>
<span class="hljs-built_in">export</span> SERVICE_ACCOUNT_NAME=<span class="hljs-string">"workload-identity-sa"</span>
<span class="hljs-built_in">export</span> SUBSCRIPTIONID=$(az account show --query id -o tsv)
<span class="hljs-built_in">export</span> AZURE_TENANT_ID=$(az account show -s <span class="hljs-variable">${SUBSCRIPTIONID}</span> --query tenantId -otsv)

<span class="hljs-comment"># Create resource group.</span>
<span class="hljs-built_in">echo</span> <span class="hljs-string">"Creating a resource group: <span class="hljs-variable">${RESOURCE_GROUP_NAME}</span>"</span>
az group create --name <span class="hljs-variable">${RESOURCE_GROUP_NAME}</span> --location <span class="hljs-variable">${LOCATION}</span>

<span class="hljs-comment"># Create AKS cluster.</span>
az aks create \
    --resource-group <span class="hljs-variable">${RESOURCE_GROUP_NAME}</span>  \
    --name <span class="hljs-variable">${CLUSTER_NAME}</span> \
    --network-plugin azure \
    --enable-managed-identity \
    --generate-ssh-keys \
    --node-count 1 \
    --enable-oidc-issuer \
    --outbound-type  loadBalancer 

<span class="hljs-comment"># Output the OIDC issuer URL.</span>
<span class="hljs-built_in">export</span> SERVICE_ACCOUNT_ISSUER=$(az aks show --resource-group <span class="hljs-variable">${RESOURCE_GROUP_NAME}</span> --name <span class="hljs-variable">${CLUSTER_NAME}</span> --query <span class="hljs-string">"oidcIssuerProfile.issuerUrl"</span> -otsv)

<span class="hljs-comment"># Get the kubeconfig for the Kubernetes clusters.</span>
az aks get-credentials --name <span class="hljs-variable">${CLUSTER_NAME}</span> --resource-group <span class="hljs-variable">${RESOURCE_GROUP_NAME}</span>

<span class="hljs-comment"># Create a new Service account.</span>
kubectl create sa <span class="hljs-variable">${SERVICE_ACCOUNT_NAME}</span> -n <span class="hljs-variable">${SERVICE_ACCOUNT_NAMESPACE}</span>
</code></pre>
<h3 id="heading-create-an-aws-resource-for-testing-the-access-from-aks">Create an AWS resource for testing the access from AKS</h3>
<p>First, you need to create an Amazon S3 bucket. Replace your bucket name with your desired bucket name and your region with the AWS region you want to use. Copy a file into the S3 bucket for testing later. Note that we created a dummy text file named <code>test-file.txt</code> for testing the access.</p>
<pre><code class="lang-bash"><span class="hljs-built_in">export</span> BUCKET_NAME=irsaaccess
<span class="hljs-built_in">export</span> REGION=us-east-1
aws s3api create-bucket --bucket <span class="hljs-variable">${BUCKET_NAME}</span> --region <span class="hljs-variable">${REGION}</span>

aws s3 cp test-file.txt s3://<span class="hljs-variable">${BUCKET_NAME}</span>/test-file.txt
</code></pre>
<h3 id="heading-create-an-aws-iam-identity-provider">Create an AWS IAM Identity provider</h3>
<p>Create an Identity provider from the AWS console with the Azure OIDC Issuer URL from the above <code>SERVICEACCOUNTISSUER</code> and audience as <a target="_blank" href="https://aws.amazon.com/iam/"><code>sts.amazonaws.com</code></a></p>
<p>You need three pieces of information:</p>
<p>1. OIDC Issuer Url from AKS.</p>
<p>2. Audience which is <code>sts.amazonaws.com</code>.</p>
<p>3. CA Root cert thumbprint.</p>
<p>The first two pieces of info exist already, so now we need to get the CA Root Cert thumbprint.</p>
<pre><code class="lang-bash"><span class="hljs-comment"># Extract the OIDC host from the issuer URL</span>
<span class="hljs-built_in">export</span> OIDC_ISSUER=$(<span class="hljs-built_in">echo</span> <span class="hljs-variable">$SERVICE_ACCOUNT_ISSUER</span> | sed <span class="hljs-string">'s|https://||'</span>)
<span class="hljs-built_in">export</span> OIDC_HOST=$(<span class="hljs-built_in">echo</span> <span class="hljs-variable">$SERVICE_ACCOUNT_ISSUER</span> | sed <span class="hljs-string">'s|https://||'</span> | cut -d<span class="hljs-string">'/'</span> -f1)

<span class="hljs-comment"># Fetch the certificate chain from the OIDC host</span>
<span class="hljs-built_in">echo</span> | openssl s_client -connect <span class="hljs-variable">$OIDC_HOST</span>:443 -servername <span class="hljs-variable">$OIDC_HOST</span> -showcerts 2&gt;/dev/null | awk <span class="hljs-string">'/-----BEGIN CERTIFICATE-----/{cert=$0 "\n"; next} /-----END CERTIFICATE-----/{cert=cert $0 "\n"; last_cert=cert; next} {cert=cert $0 "\n"} END{print last_cert &gt; "last_cert.pem"}'</span> 

<span class="hljs-comment"># Calculate the SHA-1 fingerprint of the root CA certificate and format it how AWS Expects it.</span>
CERT_THUMBPRINT=$(openssl x509 -<span class="hljs-keyword">in</span> last_cert.pem -fingerprint -noout -sha1 | sed <span class="hljs-string">'s/sha1 Fingerprint=//'</span> | tr -d <span class="hljs-string">':'</span>)

rm last_cert.pem

<span class="hljs-built_in">export</span> IAM_IDP_ARN=$(aws iam create-open-id-connect-provider --url <span class="hljs-variable">$SERVICE_ACCOUNT_ISSUER</span> --client-id-list <span class="hljs-string">"sts.amazonaws.com"</span> --thumbprint-list <span class="hljs-variable">$CERT_THUMBPRINT</span> | jq -r <span class="hljs-string">'.OpenIDConnectProviderArn'</span>)
</code></pre>
<h3 id="heading-create-an-iam-policy-to-grant-access-to-the-s3-bucket">Create an IAM Policy to grant access to the S3 bucket</h3>
<p>Use the below policy JSON and save it to a file locally as <code>s3-policy.json</code> to consume later. Note the bucket name matches the name of the S3 bucket we created earlier.</p>
<pre><code class="lang-json">{
    <span class="hljs-attr">"Version"</span>: <span class="hljs-string">"2012-10-17"</span>,
    <span class="hljs-attr">"Statement"</span>: [
        {
            <span class="hljs-attr">"Effect"</span>: <span class="hljs-string">"Allow"</span>,
            <span class="hljs-attr">"Action"</span>: [
                <span class="hljs-string">"s3:GetObject"</span>,
                <span class="hljs-string">"s3:ListBucket"</span>
            ],
            <span class="hljs-attr">"Resource"</span>: [
                <span class="hljs-string">"arn:aws:s3:::irsaaccess"</span>,
                <span class="hljs-string">"arn:aws:s3:::irsaaccess/*"</span>
            ]
        }
    ]
}
</code></pre>
<p>Run the below command to create the custom policy. Please make sure that you configure <code>aws cli</code>.</p>
<pre><code class="lang-bash"><span class="hljs-built_in">export</span> POLICY_ARN=$(aws iam create-policy --policy-name S3ListReadPolicy --policy-document file://s3-policy.json | jq -r <span class="hljs-string">'.Policy.Arn'</span>)
</code></pre>
<h3 id="heading-create-a-trust-relationship-policy-document">Create a trust relationship policy document</h3>
<p>Use the templated JSON below to create a trust policy document and save it as <code>trust-policy-template.json</code>.</p>
<pre><code class="lang-json">{
    <span class="hljs-attr">"Version"</span>: <span class="hljs-string">"2012-10-17"</span>,
    <span class="hljs-attr">"Statement"</span>: [
        {
            <span class="hljs-attr">"Effect"</span>: <span class="hljs-string">"Allow"</span>,
            <span class="hljs-attr">"Principal"</span>: {
                <span class="hljs-attr">"Federated"</span>: <span class="hljs-string">"$IAM_IDP_ARN"</span>
            },
            <span class="hljs-attr">"Action"</span>: <span class="hljs-string">"sts:AssumeRoleWithWebIdentity"</span>,
            <span class="hljs-attr">"Condition"</span>: {
                <span class="hljs-attr">"StringEquals"</span>: {
                    <span class="hljs-attr">"$OIDC_ISSUER:sub"</span>: <span class="hljs-string">"sts.amazonaws.com"</span>
                },
                <span class="hljs-attr">"ForAnyValue:StringEquals"</span>: {
                    <span class="hljs-attr">"$OIDC_ISSUER:sub"</span>: [
                        <span class="hljs-string">"system:serviceaccount:$SERVICE_ACCOUNT_NAMESPACE:$SERVICE_ACCOUNT_NAME"</span>
                    ]
                }
            }
        }
    ]
}
</code></pre>
<p>We have a templated trust policy file named <code>trust-policy-template.json</code> and using the <code>envsubst</code> CLI replace the the template values from env variables which is ready for us to consume.</p>
<pre><code class="lang-bash">envsubst &lt; trust-policy-template.json &gt; trust-policy.json
</code></pre>
<p>Now this has generated a new file <code>trust-policy.json</code> with the required configuration.</p>
<h3 id="heading-create-an-iam-role-by-attaching-a-trust-policy">Create an IAM Role by attaching a trust policy</h3>
<p>You create the role using the AWS CLI by specifying the role name and the trust relationship policy document. Save your trust policy JSON to a file, for instance, trust-policy.json, and then run the below AWS CLI command</p>
<pre><code class="lang-bash"><span class="hljs-built_in">export</span> WEB_IDENTITY_ROLE=$(aws iam create-role --role-name MyWebIdentityRole --assume-role-policy-document file://trust-policy.json | jq -r <span class="hljs-string">'.Role.Arn'</span>)
</code></pre>
<p>Once the role is created, you can attach permission policies to define what resources and actions the role can access. If you already have a policy ARN to attach, you can use:</p>
<pre><code class="lang-bash">aws iam attach-role-policy --role-name MyWebIdentityRole --policy-arn <span class="hljs-variable">$POLICY_ARN</span>
</code></pre>
<h3 id="heading-annotate-the-service-account-for-irsa">Annotate the Service account for IRSA</h3>
<pre><code class="lang-bash">kubectl annotate sa <span class="hljs-variable">${SERVICE_ACCOUNT_NAME}</span> -n <span class="hljs-variable">${SERVICE_ACCOUNT_NAMESPACE}</span> eks.amazonaws.com/role-arn=<span class="hljs-string">"<span class="hljs-variable">${WEB_IDENTITY_ROLE}</span>"</span> --overwrite
kubectl annotate sa <span class="hljs-variable">${SERVICE_ACCOUNT_NAME}</span> -n <span class="hljs-variable">${SERVICE_ACCOUNT_NAMESPACE}</span> eks.amazonaws.com/audience=<span class="hljs-string">"sts.amazonaws.com"</span> --overwrite
kubectl annotate sa <span class="hljs-variable">${SERVICE_ACCOUNT_NAME}</span> -n <span class="hljs-variable">${SERVICE_ACCOUNT_NAMESPACE}</span> eks.amazonaws.com/sts-regional-endpoints=<span class="hljs-string">"true"</span> --overwrite
kubectl annotate sa <span class="hljs-variable">${SERVICE_ACCOUNT_NAME}</span> -n <span class="hljs-variable">${SERVICE_ACCOUNT_NAMESPACE}</span> eks.amazonaws.com/token-expiration=<span class="hljs-string">"86400"</span> --overwrite
</code></pre>
<h3 id="heading-deploy-the-pod-identity-webhook">Deploy the pod identity webhook</h3>
<p>This webhook is for mutating pods that will require AWS IAM access and it can be installed using the helm chart below:</p>
<p><code>https://artifacthub.io/packages/helm/jkroepke/amazon-eks-pod-identity-webhook</code></p>
<p>Note that <code>cert-manager</code> is a pre-requisite for this add-on and you can install this from the official documentation <a target="_blank" href="https://cert-manager.io/docs/installation/">https://cert-manager.io/docs/installation/</a>.</p>
<pre><code class="lang-bash">kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.14.4/cert-manager.yaml

helm repo add jkroepke https://jkroepke.github.io/helm-charts/
helm install amazon-eks-pod-identity-webhook jkroepke/amazon-eks-pod-identity-webhook
</code></pre>
<h3 id="heading-run-a-workload-in-the-aks-that-needs-s3-bucket-access">Run a workload in the AKS that needs S3 bucket access</h3>
<p>Note that we are deploying a pod using the service account created earlier in this setup. This pod utilises the Amazon <code>aws-cli</code> image to execute some CLI commands and test the access.</p>
<pre><code class="lang-bash">cat &lt;&lt;EOF | kubectl apply -f - 
apiVersion: v1
kind: Pod
metadata:
  name: awscli
  namespace: <span class="hljs-variable">${SERVICE_ACCOUNT_NAMESPACE}</span>
  labels:
    app: awscli
spec:
  serviceAccountName: <span class="hljs-variable">${SERVICE_ACCOUNT_NAME}</span>
  containers:
  - image: amazon/aws-cli
    <span class="hljs-built_in">command</span>:
      - <span class="hljs-string">"sleep"</span>
      - <span class="hljs-string">"604800"</span>
    imagePullPolicy: IfNotPresent
    name: awscli
  restartPolicy: Always
EOF
</code></pre>
<p>After deployment, proceed to test the access with the following commands:</p>
<pre><code class="lang-bash">kubectl <span class="hljs-built_in">exec</span> -it awscli -n <span class="hljs-variable">${SERVICE_ACCOUNT_NAMESPACE}</span> -- aws sts get-caller-identity

kubectl <span class="hljs-built_in">exec</span> -it awscli -n <span class="hljs-variable">${SERVICE_ACCOUNT_NAMESPACE}</span> -- aws s3 ls s3://irsaaccess
</code></pre>
<h2 id="heading-running-the-azure-workload-identity-in-an-aws-eks-cluster">Running the Azure Workload Identity in an AWS EKS cluster</h2>
<p>The following sections cover the steps required for you to run the Azure Workload Identity in an AWS EKS cluster, from creating the AWS EKS cluster to validating access.</p>
<h3 id="heading-create-the-aws-eks-cluster">Create the AWS EKS cluster</h3>
<p>I am using <code>eksctl</code> CLI for provisioning the cluster and please setup your <code>aws cli</code> with the required credentials. In my case, I am loading a <code>default</code> profile on my machine.</p>
<pre><code class="lang-bash"><span class="hljs-built_in">export</span> AWS_DEFAULT_PROFILE=default

<span class="hljs-comment"># Define variables</span>
<span class="hljs-built_in">export</span> CLUSTER_NAME=<span class="hljs-string">"my-eks-cluster"</span>
<span class="hljs-built_in">export</span> REGION=<span class="hljs-string">"us-west-2"</span>
<span class="hljs-built_in">export</span> NODE_TYPE=<span class="hljs-string">"t3.medium"</span>
<span class="hljs-built_in">export</span> NODE_COUNT=1
<span class="hljs-built_in">export</span> KUBERNETES_VERSION=<span class="hljs-string">"1.28"</span>

<span class="hljs-comment"># Create the EKS cluster with OIDC identity provider</span>
<span class="hljs-built_in">echo</span> <span class="hljs-string">"Step 1: Creating EKS Cluster: <span class="hljs-variable">$CLUSTER_NAME</span> with OIDC identity provider"</span>
eksctl create cluster \
  --name <span class="hljs-variable">$CLUSTER_NAME</span> \
  --version <span class="hljs-variable">$KUBERNETES_VERSION</span> \
  --region <span class="hljs-variable">$REGION</span> \
  --nodegroup-name <span class="hljs-string">"standard-workers"</span> \
  --node-type <span class="hljs-variable">$NODE_TYPE</span> \
  --nodes <span class="hljs-variable">$NODE_COUNT</span> \
  --nodes-min <span class="hljs-variable">$NODE_COUNT</span> \
  --nodes-max <span class="hljs-variable">$NODE_COUNT</span> \
  --managed \
  --with-oidc

<span class="hljs-comment"># Check if the cluster was created successfully</span>
<span class="hljs-keyword">if</span> [ $? -eq 0 ]; <span class="hljs-keyword">then</span>
    <span class="hljs-built_in">echo</span> <span class="hljs-string">"EKS Cluster <span class="hljs-variable">$CLUSTER_NAME</span> created successfully."</span>
<span class="hljs-keyword">else</span>
    <span class="hljs-built_in">echo</span> <span class="hljs-string">"Failed to create EKS Cluster <span class="hljs-variable">$CLUSTER_NAME</span>."</span>
<span class="hljs-keyword">fi</span>
<span class="hljs-built_in">export</span> OIDC_URL=$(aws eks describe-cluster --name <span class="hljs-variable">$CLUSTER_NAME</span> --query <span class="hljs-string">"cluster.identity.oidc.issuer"</span> --output text)

<span class="hljs-built_in">export</span> AZURE_SUBSCRIPTION_ID=$(az account show --query id -o tsv)

<span class="hljs-built_in">export</span> AZURE_TENANT_ID=$(az account show -s <span class="hljs-variable">${AZURE_SUBSCRIPTION_ID}</span> --query tenantId -otsv)
</code></pre>
<h3 id="heading-install-azure-workload-identity">Install Azure Workload Identity</h3>
<pre><code class="lang-bash">helm repo add azure-workload-identity https://azure.github.io/azure-workload-identity/charts
helm repo update
helm install workload-identity-webhook azure-workload-identity/workload-identity-webhook \
   --namespace azure-workload-identity-system \
   --create-namespace \
   --<span class="hljs-built_in">set</span> azureTenantID=<span class="hljs-string">"<span class="hljs-variable">${AZURE_TENANT_ID}</span>"</span>
</code></pre>
<h3 id="heading-create-an-azure-keyvault-for-testing">Create an Azure KeyVault for testing</h3>
<pre><code class="lang-bash"><span class="hljs-built_in">export</span> RESOURCE_GROUP_NAME=<span class="hljs-string">"rg-aws-access-az"</span>
<span class="hljs-built_in">export</span> LOCATION=<span class="hljs-string">"eastus"</span>
<span class="hljs-built_in">export</span> KEYVAULT_NAME=<span class="hljs-string">"kv-identity-azwi1"</span>
<span class="hljs-built_in">export</span> KEYVAULT_SECRET_NAME=<span class="hljs-string">"secret"</span>

<span class="hljs-comment"># Create keyvault and secret</span>
az keyvault create --resource-group <span class="hljs-variable">${RESOURCE_GROUP_NAME}</span> \
   --location <span class="hljs-variable">${LOCATION}</span> \
   --name <span class="hljs-variable">${KEYVAULT_NAME}</span>

az keyvault <span class="hljs-built_in">wait</span> --name <span class="hljs-variable">${KEYVAULT_NAME}</span> --created

az keyvault secret <span class="hljs-built_in">set</span> --vault-name <span class="hljs-variable">${KEYVAULT_NAME}</span> \
   --name <span class="hljs-variable">${KEYVAULT_SECRET_NAME}</span> \
   --value <span class="hljs-string">"Test"</span>

<span class="hljs-built_in">export</span> KEYVAULT_URL=$(az keyvault show -g <span class="hljs-string">"rg-aks-az-identity"</span> -n <span class="hljs-variable">${KEYVAULT_NAME}</span> --query properties.vaultUri -o tsv)
</code></pre>
<h3 id="heading-create-a-managed-identity-to-federate-access-to-a-keyvault">Create a Managed Identity to federate access to a KeyVault</h3>
<pre><code class="lang-bash"><span class="hljs-built_in">export</span> IDENTITY_NAME=<span class="hljs-string">"aws-access-az"</span>

az group create --name <span class="hljs-variable">${RESOURCE_GROUP_NAME}</span> --location <span class="hljs-variable">${LOCATION}</span>

az identity create --name <span class="hljs-variable">${IDENTITY_NAME}</span> --resource-group <span class="hljs-variable">${RESOURCE_GROUP_NAME}</span> --query principalId -o tsv

<span class="hljs-built_in">export</span> USER_ASSIGNED_IDENTITY_CLIENT_ID=<span class="hljs-string">"<span class="hljs-subst">$(az identity show --name <span class="hljs-string">"<span class="hljs-variable">${IDENTITY_NAME}</span>"</span> --resource-group <span class="hljs-string">"<span class="hljs-variable">${RESOURCE_GROUP_NAME}</span>"</span> --query 'clientId' -otsv)</span>"</span>
<span class="hljs-built_in">export</span> USER_ASSIGNED_IDENTITY_OBJECT_ID=<span class="hljs-string">"<span class="hljs-subst">$(az identity show --name <span class="hljs-string">"<span class="hljs-variable">${IDENTITY_NAME}</span>"</span> --resource-group <span class="hljs-string">"<span class="hljs-variable">${RESOURCE_GROUP_NAME}</span>"</span> --query 'principalId' -otsv)</span>"</span>


az keyvault set-policy --name <span class="hljs-variable">${KEYVAULT_NAME}</span> \
  --secret-permissions get \
  --object-id <span class="hljs-variable">${USER_ASSIGNED_IDENTITY_OBJECT_ID}</span> \
  --resource-group <span class="hljs-variable">${RESOURCE_GROUP_NAME}</span>  \
  --subscription <span class="hljs-variable">${AZURE_SUBSCRIPTION_ID}</span>

az identity federated-credential create \
    --name <span class="hljs-string">"kubernetes-federated-credential"</span> \
    --identity-name <span class="hljs-variable">${IDENTITY_NAME}</span> \
    --resource-group <span class="hljs-variable">${RESOURCE_GROUP_NAME}</span> \
    --issuer <span class="hljs-variable">${OIDC_URL}</span> \
    --subject <span class="hljs-string">"system:serviceaccount:<span class="hljs-variable">${SERVICE_ACCOUNT_NAMESPACE}</span>:<span class="hljs-variable">${SERVICE_ACCOUNT_NAME}</span>"</span>
</code></pre>
<h3 id="heading-create-a-service-account-in-eks-and-annotate">Create a service account in EKS and annotate</h3>
<pre><code class="lang-bash"><span class="hljs-built_in">export</span> SERVICE_ACCOUNT_NAMESPACE=<span class="hljs-string">"azworkload"</span>
<span class="hljs-built_in">export</span> SERVICE_ACCOUNT_NAME=<span class="hljs-string">"azworkload"</span>
kubectl create ns <span class="hljs-variable">${SERVICE_ACCOUNT_NAMESPACE}</span>
kubectl create sa <span class="hljs-variable">${SERVICE_ACCOUNT_NAME}</span> -n <span class="hljs-variable">${SERVICE_ACCOUNT_NAMESPACE}</span>

kubectl annotate sa <span class="hljs-variable">${SERVICE_ACCOUNT_NAME}</span> -n <span class="hljs-variable">${SERVICE_ACCOUNT_NAMESPACE}</span> azure.workload.identity/tenant-id=<span class="hljs-string">"<span class="hljs-variable">${AZURE_TENANT_ID}</span>"</span> --overwrite
kubectl annotate sa <span class="hljs-variable">${SERVICE_ACCOUNT_NAME}</span> -n <span class="hljs-variable">${SERVICE_ACCOUNT_NAMESPACE}</span> azure.workload.identity/client-id=<span class="hljs-string">"<span class="hljs-variable">${USER_ASSIGNED_IDENTITY_CLIENT_ID}</span>"</span> --overwrite
</code></pre>
<h3 id="heading-deploy-the-workload-in-eks">Deploy the workload in EKS</h3>
<pre><code class="lang-bash">cat &lt;&lt;EOF | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata:
  name: kv-read
  namespace: <span class="hljs-variable">${SERVICE_ACCOUNT_NAMESPACE}</span>
  labels:
    azure.workload.identity/use: <span class="hljs-string">"true"</span>
spec:
  serviceAccountName: <span class="hljs-variable">${SERVICE_ACCOUNT_NAME}</span>
  containers:
    - image: ghcr.io/azure/azure-workload-identity/msal-go
      name: oidc
      env:
      - name: KEYVAULT_URL
        value: <span class="hljs-variable">${KEYVAULT_URL}</span>
      - name: SECRET_NAME
        value: <span class="hljs-variable">${KEYVAULT_SECRET_NAME}</span>
  nodeSelector:
    kubernetes.io/os: linux
EOF
</code></pre>
<h3 id="heading-validate-access">Validate access</h3>
<p>To check that we are able to access the secret from the KeyVault, check the logs of the pod <code>kv-read</code> from above which would display the secret value. Note that the image used here uses the Microsoft Authentication Library (MSAL) go library to make use of the Workload Identity to access the KeyVault.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>OIDC plays a crucial role in verifying identities across various cloud platforms, simplifying the complex process of integrating identity verification into OAuth 2.0. It allows for seamless, secure communication between cloud providers by ensuring that identities are authenticated properly before granting access to resources. This is particularly beneficial in scenarios where services from multiple cloud providers, like AWS and Azure, need to interoperate securely without sharing credentials directly.</p>
<p>Azure has abstracted much of the complexity involved in setting up the OIDC provider, offering a more user-friendly experience compared to AWS. Azure's Workload Identity integration with Azure Active Directory (AAD) provides a streamlined process where the identity management is tightly coupled with the Azure ecosystem, reducing manual configurations and potential errors.</p>
<p>AWS, while more complex in its setup, offers robust and flexible configurations through its IAM Roles for Service Accounts (IRSA) mechanism. This approach, although requiring a deeper understanding and more detailed configuration, allows for finely-tuned access control and extensive customization options, catering to advanced use cases and specific security requirements.</p>
<h2 id="heading-references">References</h2>
<ul>
<li><p><a target="_blank" href="https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_providers_create_oidc_verify-thumbprint.html">Obtain the thumbprint for an OpenID Connect identity provider</a></p>
</li>
<li><p><a target="_blank" href="https://learn.microsoft.com/en-us/azure/aks/use-oidc-issuer">Create an OpenID Connect provider on Azure Kubernetes Service (AKS)</a></p>
</li>
<li><p><a target="_blank" href="https://www.rfc-editor.org/rfc/rfc5246#section-7.4.2">Server Certificate</a></p>
</li>
<li><p><a target="_blank" href="https://aws.amazon.com/blogs/containers/diving-into-iam-roles-for-service-accounts/">Diving into IAM Roles for Service Accounts</a></p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Exploring if Bruno is a viable alternative API testing tool to Postman]]></title><description><![CDATA[By Aska Cloke This blog post is aimed at individuals who use Postman or any other API development or testing tools in their projects. Throughout this post…]]></description><link>https://nearform.hashnode.dev/exploring-if-bruno-is-a-viable-alternative-api-testing-tool-to-postman</link><guid isPermaLink="true">https://nearform.hashnode.dev/exploring-if-bruno-is-a-viable-alternative-api-testing-tool-to-postman</guid><category><![CDATA[General Programming]]></category><category><![CDATA[iwritecode]]></category><category><![CDATA[Programming Blogs]]></category><category><![CDATA[bruno]]></category><category><![CDATA[Postman]]></category><dc:creator><![CDATA[Nearform]]></dc:creator><pubDate>Fri, 05 Jul 2024 18:01:17 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1720202218513/c899c74c-bd23-473a-bb7e-0c4250de065f.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>By Aska Cloke</p>
<h2 id="heading-we-assess-if-bruno-is-able-to-do-everything-postman-does">We assess if Bruno is able to do everything Postman does</h2>
<p>This blog post is aimed at individuals who use <a target="_blank" href="https://www.postman.com/">Postman</a> or any other API development or testing tools in their projects. Throughout this post, we will explore the key features of <a target="_blank" href="https://www.usebruno.com/">Bruno</a>, an alternative tool to Postman, and highlight the significant benefits it brought to our projects.</p>
<h2 id="heading-postman-a-reliable-tool-for-api-testing">Postman, a reliable tool for API testing</h2>
<p>When it comes to API testing, Postman has been the tool of choice for countless organisations. Utilised for its easy-to-use interface, Postman simplifies the API testing process and allows you to incorporate response validation tests using JavaScript, enabling users to ensure the accuracy and reliability of API responses.</p>
<p>Another big advantage of Postman is its flexibility when using different environments. Whether you’re testing locally, on staging servers, or in production environments, Postman provides a streamlined user experience, allowing users to adapt their testing strategies with ease.</p>
<h2 id="heading-why-look-for-an-alternative-to-postman">Why look for an alternative to Postman?</h2>
<p>In one of our projects we used GraphQL queries tailored to each environment, and we needed to ensure thorough testing across different setups. We implemented an HTML report feature, offering a visually engaging summary of our findings.</p>
<p>This report not only facilitated clear communication of our test results to our stakeholders, but also aided in identifying any discrepancies or areas for improvement. We were also able to streamline our testing process by integrating our collection into the CI/CD pipeline using <a target="_blank" href="https://learning.postman.com/docs/collections/using-newman-cli/command-line-integration-with-newman/">Newman</a>. This integration allowed for nightly executions of the collection without encountering any operational issues, enabling continuous monitoring of the services' performance and stability.</p>
<p>In late 2023, Postman <a target="_blank" href="https://blog.postman.com/announcing-new-lightweight-postman-api-client">announced</a> it was making some significant modifications to its pricing and functionality structure. We were using the free desktop version, now known as the lightweight API, which became impractical for our needs.</p>
<p>While the prospect of upgrading to the enterprise version to access the necessary functionalities seemed plausible, it unveiled a dealbreaker for our client: data security.</p>
<p>Transitioning to the enterprise version mandated data synchronisation with the cloud, a feature that clashed with our client's security protocols. Further compounding this challenge was the absence of an offline version in Postman's offerings, except for the lightweight API. Consequently, we had to identify a suitable alternative tool that could accommodate our testing needs while adhering to our client's security standards.</p>
<h2 id="heading-introducing-bruno">Introducing Bruno</h2>
<p>Bruno is an alternative to Postman which offers some key features that immediately appeared useful for our migration activities:</p>
<ol>
<li><p>User interface</p>
</li>
<li><p>Importing collections</p>
</li>
<li><p>Assertions</p>
</li>
<li><p>Visual Studio Code extension</p>
</li>
<li><p>Secrets</p>
</li>
</ol>
<h3 id="heading-user-interface">User interface</h3>
<p>Bruno’s UI is very similar to Postman’s. If you’re used to using Postman, you’ll know straight away where everything is. Postman’s UI was working; it flows, so it seems like Bruno was sticking to that flow.</p>
<p>In Bruno's 'Settings' section, you'll discover a feature that allows you to include scripts for acquiring access tokens or executing other prerequisite actions. Additionally, you can select the authentication mode for the entire collection. Interestingly, this functionality now appears to be absent from Postman's free version, marking a notable advantage for Bruno users.</p>
<p>When it comes to the query section, Bruno closely resembles Postman, with one notable addition: the 'Assertion' section. This extra component provides users with a dedicated space to define assertions, allowing for more comprehensive and efficient testing. We'll delve deeper into this feature later on.</p>
<h3 id="heading-importing-collections">Importing collections</h3>
<p>The ability to import existing collections from Postman proved invaluable, particularly given our prior investment in Postman. Using this functionality, we migrated our extensive collection from Postman to Bruno.</p>
<p>However, it's worth noting that some adjustments were necessary during the migration process. Postman’s folder section of pre-script did not get imported, so we had to rewrite it, which we needed to acquire access tokens for our APIs. Additionally, Bruno employs its own set of keywords and conventions, necessitating updates to align our tests to keep them working.</p>
<p>Nevertheless, despite these minor modifications, the migration process was smooth and efficient, enabling us to swiftly transition our testing infrastructure to Bruno's platform without any impact on the validity or functionality of our test suites.</p>
<p><img src="https://www.nearform.com/media/pages/digital-community/exploring-if-bruno-is-a-viable-alternative-api-testing-tool-to-postman/a6190f2864-1720200228/blog-exploring-if-bruno-is-a-viable-alternative-api-testing-tool-to-postman-importing-collections-1600x.png" alt="A screenshot from the Bruno API testing tool showing an example of 'importing collections'" /></p>
<h3 id="heading-assertions">Assertions</h3>
<p>As a test engineer, I loved this feature that Postman does not have. It allows users to effortlessly build tests one after another, simplifying the testing process significantly.</p>
<p><img src="https://www.nearform.com/media/pages/digital-community/exploring-if-bruno-is-a-viable-alternative-api-testing-tool-to-postman/37bc295186-1720200382/blog-exploring-if-bruno-is-a-viable-alternative-api-testing-tool-to-postman-importing-collections-films-1600x.png" alt="A screenshot from the Bruno API testing tool lines of code for films being imported " /></p>
<p>Despite my appreciation for this feature, its utilisation was not necessary for our project at the time. Given that we already had an extensive collection established in Postman, our approach involved importing this existing collection into Bruno and subsequently updating it to align with Bruno.</p>
<p>The screenshot below shows Postman’s tests that we imported into Bruno. As you can see, you cannot use them as they are. We updated all our tests using <a target="_blank" href="https://en.wikipedia.org/wiki/Grep">grep</a> which took only a small amount of time to make them executable again.</p>
<p><img src="https://www.nearform.com/media/pages/digital-community/exploring-if-bruno-is-a-viable-alternative-api-testing-tool-to-postman/97944da46a-1720200540/blog-exploring-if-bruno-is-a-viable-alternative-api-testing-tool-to-postman-importing-collections-test-comparison-1600x.png" alt="A screenshot showing imported Bruno tests and updated Postman tests for the API testing tools" /></p>
<h3 id="heading-visual-studio-code-extension">Visual Studio Code extension</h3>
<p>Postman offered an 'export' function, enabling users to work with exported Postman collections in Visual Studio Code. However, the current version has removed this functionality, making it impossible to share collections through Visual Studio Code.</p>
<p>Bruno comes with Visual Studio Extension. What truly set this feature apart was its synchronisation with the UI. Any modifications made within the UI were automatically reflected in the corresponding files. All the essential components, header data, request body, variables, and tests, were neatly organised in one file.</p>
<p><img src="https://www.nearform.com/media/pages/digital-community/exploring-if-bruno-is-a-viable-alternative-api-testing-tool-to-postman/13fa4b4174-1720200734/blog-exploring-if-bruno-is-a-viable-alternative-api-testing-tool-to-postman-importing-collections-visual-sudio-code-1600x.png" alt="A screenshot of the Visual Studio Extension feature of the Bruno API testing tool " /></p>
<h3 id="heading-secrets">Secrets</h3>
<p>Within Bruno’s configuration settings, users have the capability to hide the values of variables designated as sensitive.</p>
<p>This functionality ensures that these values remain safeguarded within the environment settings. Consequently, they are accessible for local usage while being shielded from inadvertent exposure when pushing changes to remote repositories. The ability to hide secret values in this manner provides an added layer of security and compliance, aligning with industry best practices for safeguarding sensitive data.</p>
<p><img src="https://www.nearform.com/media/pages/digital-community/exploring-if-bruno-is-a-viable-alternative-api-testing-tool-to-postman/ac00cdce84-1720200887/blog-exploring-if-bruno-is-a-viable-alternative-api-testing-tool-to-postman-env-file-1600x.png" alt="A screenshot of the Env File of the Visual Studio Extension feature of the Bruno API testing tool " /></p>
<h2 id="heading-did-bruno-replace-postman-successfully">Did Bruno replace Postman successfully?</h2>
<p>The short answer is yes; it did for our project. Bruno was able to do everything Postman did.</p>
<p>The longer answer is that during the initial phases of our migration, we encountered a hurdle in extracting environment variables within our CI/CD pipeline. This posed a significant challenge, as integration with our automated pipeline was crucial for maintaining testing efficiency and workflow continuity.</p>
<p>Fortunately, Bruno's status as a relatively new tool with an engaged community proved to be advantageous. The platform undergoes frequent updates, driven by user feedback and community collaboration. As luck would have it, one of the recent updates addressed the issue we encountered, providing the solution needed to proceed with migrating our Postman collections to Bruno.</p>
<p>In addition, Bruno's user interface proved to be intuitive and user-friendly, particularly for those accustomed to Postman. The integration with a VS Code Extension enabled collaboration and sharing among team members, encouraging more engineers to actively contribute to the collection.</p>
<p>Previously, the responsibility for updating the collection primarily fell on test engineers in our project. However, post-migration developers became more involved in updating the collections alongside their application code changes, as both files resided in the same repository. This shift fostered greater collaboration between developers and test engineers, resulting in a smoother and more efficient testing process.</p>
<p>Following the migration, our API tests continued to run without issues. The increased collaboration between developers and test engineers contributed to the overall success of the migration. Encouraged by the smooth migration and the continuous evolution of Bruno, we recommend you give Bruno a try.</p>
]]></content:encoded></item><item><title><![CDATA[Nearform's Open Source workshops]]></title><description><![CDATA[By Marco Ippolito Are you a developer eager to expand your skill set or dive into cutting-edge technologies? Look no further than Nearform's collection of…]]></description><link>https://nearform.hashnode.dev/nearforms-open-source-workshops</link><guid isPermaLink="true">https://nearform.hashnode.dev/nearforms-open-source-workshops</guid><category><![CDATA[iwritecode]]></category><category><![CDATA[General Programming]]></category><category><![CDATA[Programming Blogs]]></category><category><![CDATA[Open Source]]></category><category><![CDATA[fastify]]></category><dc:creator><![CDATA[Nearform]]></dc:creator><pubDate>Tue, 25 Jun 2024 16:55:54 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1719334392830/e6757add-7b96-4ddd-8166-4c71fa45eebd.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>By Marco Ippolito</p>
<h2 id="heading-nearform-has-curated-an-impressive-array-of-workshops-covering-a-diverse-range-of-topics">Nearform has curated an impressive array of workshops covering a diverse range of topics</h2>
<p>Are you a developer eager to expand your skill set or dive into cutting-edge technologies? Look no further than Nearform's collection of open-source workshops!</p>
<p>With a commitment to promoting learning within the developer community, Nearform has curated an impressive array of workshops covering a diverse range of topics, from backend frameworks like Fastify to security essentials like OWASP Top Ten and everything in between.</p>
<p>Each workshop is designed to combine theoretical knowledge with hands-on practice, ensuring participants gain a comprehensive understanding of the topic at hand. Participants are actively engaged in writing code and running tests to validate their understanding and mastery of the concepts covered.</p>
<p>With solutions readily available, participants can confidently solve each exercise knowing that assistance is just a step away. Let's take a closer look at some of the workshops.</p>
<h2 id="heading-fastify-workshop">Fastify Workshop</h2>
<p><a target="_blank" href="https://fastify.dev/">Fastify</a>, a web framework for Node.js, boasts lightning-fast performance and a minimalist design. <a target="_blank" href="https://github.com/nearform/the-fastify-workshop">Nearform's Fastify Workshop</a> provides a comprehensive introduction to building web applications with Fastify. Whether you're a beginner or an experienced developer, this workshop offers valuable insights and hands-on exercises to master Fastify's features and best practices.</p>
<p>Here is an example of an exercise you can find in the workshop:</p>
<p><img src="https://www.nearform.com/media/pages/digital-community/nearform-s-open-source-workshops/cb603bfcdd-1719333243/blog-nearform-open-source-workshops-1-1600x.png" alt /></p>
<p>If need some help, you can find the solution in the source code:</p>
<pre><code class="lang-js"><span class="hljs-keyword">import</span> S <span class="hljs-keyword">from</span> <span class="hljs-string">'fluent-json-schema'</span>

<span class="hljs-keyword">const</span> schema = {
  <span class="hljs-attr">body</span>: S.object()
    .prop(<span class="hljs-string">'username'</span>, S.string().required())
    .prop(<span class="hljs-string">'password'</span>, S.string().required()),
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">login</span>(<span class="hljs-params">fastify</span>) </span>{
  fastify.post(
    <span class="hljs-string">'/login'</span>,
    { schema },
    <span class="hljs-keyword">async</span> req =&gt; {
      <span class="hljs-keyword">const</span> { username, password } = req.body
      <span class="hljs-keyword">return</span> { username, password }
    },
  )
}
</code></pre>
<p>And then you can test it with Postman or Curl:</p>
<p><img src="https://www.nearform.com/media/pages/digital-community/nearform-s-open-source-workshops/36ef737371-1719334110/blog-nearform-open-source-workshops-6-1600x.png" alt /></p>
<h2 id="heading-owasp-top-ten-workshop">OWASP Top Ten Workshop</h2>
<p>Security is paramount in today's digital landscape, and <a target="_blank" href="https://github.com/nearform/owasp-top-ten-workshop">Nearform's OWASP Top Ten Workshop</a> equips developers with the essential knowledge to identify and mitigate common security vulnerabilities. By exploring the OWASP Top Ten risks, participants learn how to secure their applications and protect against threats effectively.</p>
<p>For example, in this exercise you have to fix a snippet of code with a <a target="_blank" href="https://owasp.org/www-community/attacks/SQL_Injection">SQL Injection</a> vulnerability:</p>
<pre><code class="lang-js"><span class="hljs-keyword">import</span> errors <span class="hljs-keyword">from</span> <span class="hljs-string">'http-errors'</span>

<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">customer</span>(<span class="hljs-params">fastify</span>) </span>{
  fastify.get(
    <span class="hljs-string">'/customer'</span>,
    {
      <span class="hljs-attr">onRequest</span>: [fastify.authenticate]
    },
    <span class="hljs-keyword">async</span> req =&gt; {
      <span class="hljs-keyword">const</span> { name } = req.query
      <span class="hljs-keyword">const</span> { <span class="hljs-attr">rows</span>: customers } = <span class="hljs-keyword">await</span> fastify.pg.query(
        <span class="hljs-string">`SELECT * FROM customers WHERE name='<span class="hljs-subst">${name}</span>'`</span>
      )
      <span class="hljs-keyword">if</span> (!customers.length) <span class="hljs-keyword">throw</span> errors.NotFound()
      <span class="hljs-keyword">return</span> customers
    }
  )
}
</code></pre>
<p>Let’s fix it by sanitising the user input:</p>
<pre><code class="lang-js"><span class="hljs-keyword">import</span> SQL <span class="hljs-keyword">from</span> <span class="hljs-string">'@nearform/sql'</span>
<span class="hljs-keyword">import</span> errors <span class="hljs-keyword">from</span> <span class="hljs-string">'http-errors'</span>

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">customer</span>(<span class="hljs-params">fastify</span>) </span>{
  fastify.get(
    <span class="hljs-string">'/customer'</span>,
    {
      <span class="hljs-attr">onRequest</span>: [fastify.authenticate]
    },
    <span class="hljs-keyword">async</span> req =&gt; {
      <span class="hljs-keyword">const</span> { name } = req.query
      <span class="hljs-keyword">const</span> { <span class="hljs-attr">rows</span>: customers } = <span class="hljs-keyword">await</span> fastify.pg.query(
        SQL<span class="hljs-string">`SELECT * FROM customers WHERE name=<span class="hljs-subst">${name}</span>`</span> <span class="hljs-comment">// SQL function from @nearform/sql</span>
      )
      <span class="hljs-keyword">if</span> (!customers.length) <span class="hljs-keyword">throw</span> errors.NotFound()
      <span class="hljs-keyword">return</span> customers
    }
  )
}
</code></pre>
<p>To make sure you have fixed the vulnerability correctly, you can run <code>npm run verify</code> , which will test that your solutions address the security issue in the code.</p>
<h2 id="heading-graphql-workshop">GraphQL Workshop</h2>
<p>GraphQL has revolutionised the way developers design and query APIs, offering flexibility and efficiency unmatched by traditional RESTful approaches. <a target="_blank" href="https://github.com/nearform/the-graphql-workshop">Nearform's GraphQL Workshop</a> demystifies this powerful technology, guiding developers through the fundamentals of schema design, querying, and optimisation techniques.</p>
<p>Loaders are an amazing feature of GraphQL, in this exercise you will learn how to use them correctly:</p>
<p><img src="https://www.nearform.com/media/pages/digital-community/nearform-s-open-source-workshops/5ca86cbeae-1719333502/blog-nearform-open-source-workshops-2-1600x.png" alt /></p>
<p>And then you can check the solution and run the test:</p>
<pre><code class="lang-js"><span class="hljs-keyword">const</span> pets = [
  {
    <span class="hljs-attr">name</span>: <span class="hljs-string">'Max'</span>
  },
  {
    <span class="hljs-attr">name</span>: <span class="hljs-string">'Charlie'</span>
  }
]

<span class="hljs-keyword">const</span> owners = {
  <span class="hljs-attr">Max</span>: {
    <span class="hljs-attr">name</span>: <span class="hljs-string">'Jennifer'</span>
  },
  <span class="hljs-attr">Charlie</span>: {
    <span class="hljs-attr">name</span>: <span class="hljs-string">'Simon'</span>
  }
}

<span class="hljs-keyword">const</span> schema = <span class="hljs-string">`
  type Person {
    name: String!
  }

  type Pet {
    name: String!
    owner: Person
  }

  type Query {
    pets: [Pet]
  }
`</span>

<span class="hljs-keyword">const</span> resolvers = {
  <span class="hljs-attr">Query</span>: {
    pets() {
      <span class="hljs-keyword">return</span> pets
    }
  }
}

<span class="hljs-keyword">const</span> loaders = {
  <span class="hljs-attr">Pet</span>: {
    <span class="hljs-keyword">async</span> owner(queries) {
      <span class="hljs-keyword">return</span> queries.map(<span class="hljs-function">(<span class="hljs-params">{ obj: pet }</span>) =&gt;</span> owners[pet.name])
    }
  }
}

<span class="hljs-keyword">export</span> { schema, resolvers, loaders }
</code></pre>
<h2 id="heading-node-test-runner-workshop">Node Test Runner Workshop</h2>
<p>Testing is an integral part of the software development process, ensuring the reliability and robustness of your applications.</p>
<p>Node.js released an <a target="_blank" href="https://nodejs.org/en/blog/announcements/v18-release-announce#test-runner-module-experimental">experimental test runner in version 18</a> and made that <a target="_blank" href="https://nodejs.org/en/blog/announcements/v20-release-announce#stable-test-runner">test runner stable in version 20</a>, allowing developers to test their applications without the need for external dependencies.</p>
<p><a target="_blank" href="https://github.com/nearform/node-test-runner-workshop">Nearform's Node Test Runner Workshop</a> introduces developers to various testing methodologies and tools, empowering them to write comprehensive test suites and automate testing workflows effectively.</p>
<p>In this slide we explain the difference between a Test Runner and a Testing Framework:</p>
<p><img src="https://www.nearform.com/media/pages/digital-community/nearform-s-open-source-workshops/f4ba636b96-1719333611/blog-nearform-open-source-workshops-3-1600x.png" alt /></p>
<h2 id="heading-micro-frontends-workshop">Micro Frontends Workshop</h2>
<p>Micro Frontends architecture enables teams to independently develop, deploy, and scale frontend components, fostering agility and collaboration in large-scale projects. <a target="_blank" href="https://github.com/nearform/the-micro-frontends-workshop">Nearform's Micro Frontends Workshop</a> guides developers through the principles and implementation strategies of micro frontend architecture, equipping them with the knowledge to build modular and scalable frontend systems.</p>
<p><img src="https://www.nearform.com/media/pages/digital-community/nearform-s-open-source-workshops/8d6a08efee-1719333670/blog-nearform-open-source-workshops-4-1600x.png" alt /></p>
<h2 id="heading-react-native-workshop">React Native Workshop</h2>
<p>With the rise of mobile app development, React Native has emerged as a popular framework for building cross-platform applications with JavaScript and React. <a target="_blank" href="https://github.com/nearform/react-native-workshop">Nearform's React Native Workshop</a>empowers developers to harness the full potential of React Native, from setting up development environments to building native-quality mobile experiences.</p>
<p><img src="https://www.nearform.com/media/pages/digital-community/nearform-s-open-source-workshops/1ec6da273f-1719333770/blog-nearform-open-source-workshops-5-1600x.png" alt /></p>
<h2 id="heading-immerse-yourself-in-nearforms-workshops">Immerse yourself in Nearform’s workshops</h2>
<p>Whether you're diving into backend development, exploring emerging technologies, or fortifying your application's security, Nearform's open-source workshops provide invaluable resources and guidance. Immerse yourself in these workshops, join a vibrant developer community and commence a journey of perpetual learning and advancement.</p>
<p><a target="_blank" href="https://github.com/search?q=topic:workshop+org:nearform+fork:true&amp;type=repositories">Here</a> you can find the list to all our workshops.</p>
<p>So, which workshop will you venture into first? Let's embark on this learning journey together!</p>
]]></content:encoded></item><item><title><![CDATA[OPA policy-based testing of Helm charts]]></title><description><![CDATA[By Iheanyi Onwubiko Managing Kubernetes applications correctly is as crucial as their development. Helm charts are a pivotal tool for deploying and…]]></description><link>https://nearform.hashnode.dev/opa-policy-based-testing-of-helm-charts</link><guid isPermaLink="true">https://nearform.hashnode.dev/opa-policy-based-testing-of-helm-charts</guid><category><![CDATA[iwritecode]]></category><category><![CDATA[Programming Blogs]]></category><category><![CDATA[General Programming]]></category><category><![CDATA[helm chart]]></category><category><![CDATA[opa]]></category><dc:creator><![CDATA[Nearform]]></dc:creator><pubDate>Fri, 21 Jun 2024 09:37:08 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1718962294997/6de4e77c-0869-4e2e-a307-0fda24e70651.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>By Iheanyi Onwubiko</p>
<h2 id="heading-enhancing-helm-chart-security-and-policy-compliance-with-opa">Enhancing Helm chart security and policy compliance with OPA</h2>
<p><strong>Managing Kubernetes applications correctly is as crucial as their development. Helm charts are a pivotal tool for deploying and managing these applications efficiently.</strong></p>
<p>However, ensuring that these deployments are secure and adhere to best practices requires robust oversight. This is where the <a target="_blank" href="https://www.openpolicyagent.org/">Open Policy Agent (OPA)</a> plays a vital role by enforcing a set of standardised rules.</p>
<h2 id="heading-leveraging-conftest-for-opa-policy-enforcement">Leveraging Conftest for OPA policy enforcement</h2>
<p><a target="_blank" href="https://www.conftest.dev/">Conftest</a> is a tool that supports DevOps teams by testing structured configuration data against predefined policies, ensuring configurations are correct prior to deployment. These policies, written in the <a target="_blank" href="https://www.openpolicyagent.org/docs/latest/policy-language/">Rego</a> language from OPA, consist of rules that define the necessary logic to enforce desired behaviours. Together, Conftest and OPA create a powerful ecosystem for automating and managing policy enforcement effectively.</p>
<h2 id="heading-the-benefits-of-integrating-opa-with-helm">The benefits of integrating OPA with Helm</h2>
<p>Integrating OPA with Helm chart development enhances Kubernetes management by automating the validation of the Helm templates. This approach not only addresses the challenges of manual checks, but also mitigates potential inconsistencies as Kubernetes environments scale. OPA's policy-as-code model assures uniform policy application across clusters.</p>
<h2 id="heading-preparing-your-environment">Preparing your environment</h2>
<p>To integrate Open Policy Agent (OPA) with Helm, you will need several tools and a basic understanding of key technologies:</p>
<ol>
<li><p><strong>Helm</strong>: <a target="_blank" href="https://helm.sh/docs/intro/install/">Helm installation guide</a>.</p>
</li>
<li><p><strong>Conftest</strong>: <a target="_blank" href="https://www.conftest.dev/install/">Conftest installation guide</a>.</p>
</li>
<li><p><strong>Local Kubernetes cluster</strong>: Optionally, use <a target="_blank" href="https://minikube.sigs.k8s.io/docs/start/">Minikube</a> or <a target="_blank" href="https://kind.sigs.k8s.io/">Kind</a> for realistic testing and validation of Helm charts.</p>
</li>
</ol>
<p>Additionally, familiarity with writing OPA policies in Rego, understanding Helm chart structures and Kubernetes resource management is important. In essence, what we are trying to achieve with this is to validate the final Kubernetes manifests; thus knowledge of the chart will help to fix issues.</p>
<p>For practical demonstrations, I'll reference the <a target="_blank" href="https://github.com/nearform/helm-OPA-policy-testing-templates">helm-OPA-policy-testing-templates</a> repository on GitHub, which showcases example policies to maintain standards and recommended practices in Kubernetes.</p>
<h2 id="heading-repository-structure">Repository structure</h2>
<ul>
<li><p><strong>mars/</strong>: Contains the Helm chart that will be tested — “mars” is just a random name for the chart to be tested.</p>
<ul>
<li><p><strong>templates/</strong>: Includes Kubernetes resource templates such as ConfigMaps and Ingresses.</p>
</li>
<li><p><strong>chart.yaml</strong>: Details the chart version and metadata.</p>
</li>
<li><p><strong>values.yaml</strong>: Holds default configuration values.</p>
</li>
</ul>
</li>
<li><p><strong>policy/</strong>: Includes OPA policies structured by enforcement type:</p>
<ul>
<li><p><strong>deny/</strong>: Policies that block deployments under specific conditions.</p>
</li>
<li><p><strong>violation/</strong>: Policies that identify non-adherence to best practices.</p>
</li>
<li><p><strong>warn/</strong>: Policies that issue warnings for potential issues.</p>
</li>
<li><p><strong>helpers/</strong>: Common functions used across various policies.</p>
</li>
</ul>
</li>
</ul>
<h3 id="heading-leveraging-helper-functions-in-conftest">Leveraging helper functions in Conftest</h3>
<p>Helper functions in Conftest streamline policy management by consolidating common logic into reusable components. It simplifies policy creation, enhancing clarity and manageability. The helper functions we will use in the upcoming example scenario are in the kubernetes.rego file in the repository. For detailed examples and guidelines on using these functions, please see the <a target="_blank" href="https://github.com/nearform/helm-OPA-policy-testing-templates/tree/main/policy/helpers">helper functions</a> documentation.</p>
<h2 id="heading-scenario-enforcing-key-policies-with-conftest">Scenario: Enforcing key policies with Conftest</h2>
<p>In this example scenario, we'll use Conftest to enforce two key policies for developing our Helm charts. Firstly, we prohibit the use of the 'latest' image tag for all containers to prevent unpredictability in deployments. Secondly, we mandate that all Kubernetes deployments include specific labels to improve management, visibility and recommended best practices.</p>
<p><strong><em>Prohibiting 'latest' tag policy*</em></strong>:*</p>
<pre><code class="lang-text">package main

import data.kubernetes

name := input.metadata.name

violation[msg] {
    kubernetes.containers[container]
    [image_name, "latest"] = kubernetes.split_image(container.image)
    msg = kubernetes.format(sprintf("%s in the %s %s has an image, %s, using the latest tag", [container.name, kubernetes.kind, image_name, kubernetes.name]))
}
</code></pre>
<p>This policy achieves the following:</p>
<ul>
<li><p><code>kubernetes.containers[container]</code>: Iterates over each container.</p>
</li>
<li><p><code>[imagename, "latest"] = kubernetes.splitimage(container.image)</code>: Splits the container image string to check if it uses the "latest" tag.</p>
</li>
<li><p><code>msg = kubernetes.format(sprintf(...))</code>: Generates a standardised error message if a container uses the "latest" tag, detailing the container's name and the type of Kubernetes object it belongs to.</p>
</li>
</ul>
<p><em>Enforce recommended labels policy:</em></p>
<pre><code class="lang-text">package main

import data.kubernetes

name := input.metadata.name

deny[msg] {
    kubernetes.is_deployment
    not kubernetes.required_deployment_labels
    msg = sprintf("%s must include Kubernetes recommended labels: https://kubernetes.io/docs/concepts/overview/working-with-objects/common-labels/#labels", [name])
}
</code></pre>
<p>This policy does the following:</p>
<ul>
<li><p><code>kubernetes.is_deployment</code>: Confirms the resource is a Kubernetes deployment.</p>
</li>
<li><p><code>not kubernetes.requireddeploymentlabels</code>: Condition checks whether the deployment is missing the necessary labels. The <code>requireddeploymentlabels</code> function, which performs this verification, is defined in the <a target="_blank" href="https://github.com/nearform/helm-OPA-policy-testing-templates/tree/main/policy/helpers">helper functions</a> file.</p>
</li>
<li><p><code>msg = sprintf(...)</code>: Generates an error message for non-compliant deployments, directing users to a URL with labelling best practices.</p>
</li>
</ul>
<h3 id="heading-how-to-apply-these-policies"><strong>How to apply these policies</strong>:</h3>
<p>Save the policies in a <code>.rego</code> file within a "policy" folder. Conftest defaults to searching for rules in a directory named "policy." To specify a different location, use the <code>--policy</code> or <code>-p</code> flag when running tests.</p>
<p>Run the following command from the root of your repository to test Helm charts against the defined OPA policies:</p>
<pre><code class="lang-bash">helm template mars | conftest <span class="hljs-built_in">test</span> -p policy/ -
</code></pre>
<p>This command sequence ensures your Helm chart configurations comply with OPA policies before deployment:</p>
<ul>
<li><p><code>helm template mars</code>: Generates Kubernetes YAML files from the <code>mars</code> Helm chart.</p>
</li>
<li><p><code>|</code> (pipe): Passes the YAML output to the next command.</p>
</li>
<li><p><code>conftest test</code>: Uses Conftest to evaluate the YAML files against the policies in the <code>policy/</code> directory.</p>
</li>
<li><p><code>-p policy/</code>: Specifies the directory where the policy files are located.</p>
</li>
<li><p><code>-</code>: Indicates that Conftest should read input from the standard input (stdin), provided by the pipe.</p>
</li>
</ul>
<p>You should see an output similar to the screenshot below, indicating policy enforcement failures:</p>
<pre><code class="lang-text">FAIL - - main - release-name-mars must include Kubernetes recommended labels: https://kubernetes.io/docs/concepts/overview/working-with-objects/common-labels/#labels
FAIL - - main - mars in the Deployment nginx has an image, release-name-mars, using the latest tag
</code></pre>
<p>This indicates that our Helm charts do not comply with our policies. For example, as highlighted in the <code>_helpers.tpl file</code>, there are missing deployment labels:</p>
<p><img src="https://www.nearform.com/media/pages/digital-community/opa-policy-based-testing-of-helm-charts/841e8bf201-1718959240/blog-opa-policy-based-testing-of-helm-charts-fig-1-missing-required-labels-1600x.png" alt="Helm charts missing required labels" /></p>
<p>'Fig 1: Missing required labels'</p>
<p>Furthermore, in the <code>values.yml</code> file, it appears that no specific image tag is defined, which defaults to using the "latest" tag:</p>
<p><img src="https://www.nearform.com/media/pages/digital-community/opa-policy-based-testing-of-helm-charts/539ffafe1e-1718959415/blog-opa-policy-based-testing-of-helm-charts-fig-1-missing-required-labels-no-image-tag-specified-1600x.png" alt="Helm chart undefined specific image tag" /></p>
<p>'Fig 2: Undefined specific image tag'</p>
<p>To align our Helm charts with the policies, we need to define the deployment labels in accordance to what the policy expects, and specify an image tag. We will now update the respective files, as highlighted in the images below:</p>
<p><img src="https://www.nearform.com/media/pages/digital-community/opa-policy-based-testing-of-helm-charts/4a2bbb7154-1718959571/blog-opa-policy-based-testing-of-helm-charts-fig-3-defined-required-labels-1600x.png" alt="Helm charts defined required labels" /></p>
<p>‘Fig 3: Defined required labels’</p>
<p><img src="https://www.nearform.com/media/pages/digital-community/opa-policy-based-testing-of-helm-charts/8a0d956419-1718959675/blog-opa-policy-based-testing-of-helm-charts-fig-4-image-tag-specified-1600x.png" alt="Helm charts image with a specific tag" /></p>
<p>‘Fig 4: Image with a specific tag’</p>
<p>After implementing these fixes, rerunning the command <code>helm template mars | conftest test -p policy/ -</code> should not yield any policy failures.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>This blog has highlighted the benefits of integrating Open Policy Agent (OPA) with Helm chart development to safeguard Kubernetes deployments. By enforcing standardised policies using Conftest and OPA, teams can ensure their Helm charts meet high security and compliant standards. Moreover, DevOps teams can integrate this mechanism into their GitOps workflow to streamline template validation and necessary security checks, embedding them directly into the deployment process.</p>
]]></content:encoded></item><item><title><![CDATA[Revolutionising business data management with Nearform's Generative AI: Introducing the pgRAG AI accelerator]]></title><description><![CDATA[By Cristian Barlutiu At Nearform, we engage with the latest technological trends and innovations. A key focus for us is on Generative AI (GenAI), a…]]></description><link>https://nearform.hashnode.dev/revolutionising-business-data-management-with-nearforms-generative-ai-introducing-the-pgrag-ai-accelerator</link><guid isPermaLink="true">https://nearform.hashnode.dev/revolutionising-business-data-management-with-nearforms-generative-ai-introducing-the-pgrag-ai-accelerator</guid><category><![CDATA[General Programming]]></category><category><![CDATA[iwritecode]]></category><category><![CDATA[Programming Blogs]]></category><category><![CDATA[generative ai]]></category><category><![CDATA[RAG ]]></category><dc:creator><![CDATA[Nearform]]></dc:creator><pubDate>Fri, 14 Jun 2024 10:01:21 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1718359092991/80d4661c-bf2a-402d-a5ba-d67615bc6d2b.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>By Cristian Barlutiu</p>
<h2 id="heading-discover-how-nearforms-gen-ai-innovations-are-revolutionising-business-data-management-and-automation">Discover how Nearform's Gen AI innovations are revolutionising business data management and automation</h2>
<p><strong>At Nearform, we engage with the latest technological trends and innovations. A key focus for us is on Generative AI (GenAI), a technology that's transforming how businesses create and handle information. We're actively exploring its potential, particularly in using machine learning to create new content, such as text, images and even code.</strong></p>
<p>This technology is already revolutionising how businesses handle information, and we've been at the forefront of its application. We're constantly exploring new ways to leverage it to empower our clients and unlock the hidden potential within their data.</p>
<p>Generative AI utilises advanced algorithms to create new content, translating to a wealth of benefits for companies. Imagine instantly summarising complex documents, extracting key insights from vast datasets, or generating creative content tailored to specific needs. This opens doors to improvements in efficiency, data-driven decision-making and a whole new level of automation.</p>
<h2 id="heading-our-solution-the-pgrag-ai-accelerator-library">Our solution: The pgRAG AI accelerator library</h2>
<p>With <strong>pgRAG</strong>, Nearform is pushing the boundaries of Generative AI, solving common challenges faced by developers and streamlining AI implementation. We've seen GenAI’s transformative power firsthand across numerous client projects. However, we noticed recurring challenges – developers often needed to reinvent the wheel for common GenAI use cases. This sparked the creation of <strong>pgRAG</strong>, our in-house AI accelerator library.</p>
<h2 id="heading-how-does-pgrag-work">How does pgRAG work?</h2>
<p><img src="https://www.nearform.com/media/pages/digital-community/revolutionising-business-data-management-with-nearform-s-generative-ai-introducing-the-pgrag-ai-accelerator/35d3c2e05c-1718352396/blog-revolutionising-business-data-management-with-nearforms-generative-aipgrag-flow-chart-1600x.png" alt="The pgRag data flow chart, all the date is stored using a Postgres database" /></p>
<p>The <strong>pgRAG</strong> library excels in text analysis and summarisation, supporting various document formats like PDFs, Word documents and PowerPoint presentations. It employs cutting-edge natural language processing techniques to perform semantic searches and extract critical information seamlessly. It leverages advanced natural language processing techniques to perform semantic searches and extract key information from PDFs, Word documents, PowerPoint presentations and more.</p>
<p><img src="https://www.nearform.com/media/pages/digital-community/revolutionising-business-data-management-with-nearform-s-generative-ai-introducing-the-pgrag-ai-accelerator/16e961dea7-1718352656/blog-revolutionising-business-data-management-with-nearforms-generative-aipgrag-flow-chart-pgrag-example-1600x.png" alt="An example of how pgRAG leverages advanced natural language processing techniques to perform semantic searches and extract key information from" /></p>
<p>One of the core features is the ability to generate concise summaries that capture the essence of the document's content. To ensure accuracy, the library employs carefully designed prompts during the summarisation process, mitigating the risk of hallucinations or factual inconsistencies that can sometimes occur with Generative AI models.</p>
<p>What sets <strong>pgRAG</strong> apart is its capability to analyse and interpret visual elements within documents, such as charts, graphs and images. The AI can extract insights from these visual representations and incorporate them into the overall document summary, providing a more comprehensive understanding of the content.</p>
<p>For instance, when processing a complex financial report, <strong>pgRAG</strong> can analyse both the textual data and accompanying visualisations like charts and graphs. It then generates a concise summary that captures the key points from the text and insights from the visual elements, presenting decision-makers with a holistic overview of the report.</p>
<p>JavaScript Copy to clipboard</p>
<pre><code class="lang-js"><span class="hljs-comment">// configuration</span>
<span class="hljs-keyword">const</span> model = <span class="hljs-keyword">new</span> OpenAI{...});
<span class="hljs-keyword">const</span> embeddings = <span class="hljs-keyword">new</span> OpenAIEmbeddings({...});
<span class="hljs-keyword">const</span> dbPool = <span class="hljs-keyword">new</span> pg.Pool({...});

<span class="hljs-comment">// read the document file</span>
<span class="hljs-keyword">const</span> myDocument = fs.readFileSync(<span class="hljs-string">'research_paper.pdf'</span>);

<span class="hljs-comment">// initialize pgRag</span>
<span class="hljs-keyword">const</span> pgRagInstance = <span class="hljs-keyword">await</span> PgRag.init({
  embeddings,
  model,
  dbPool
});

<span class="hljs-comment">// process the document </span>
<span class="hljs-keyword">const</span> jobId = <span class="hljs-keyword">await</span> pgRagInstance.processDocument({
  <span class="hljs-attr">data</span>: myDocument, 
  <span class="hljs-attr">name</span>: <span class="hljs-string">'research_paper.pdf'</span>
});

<span class="hljs-comment">// document searching</span>
<span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> pgRagInstance.rag({
  <span class="hljs-attr">prompt</span>: <span class="hljs-string">'Tell me about molecular structure of glucose'</span>
});
<span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Search response'</span>, response);

<span class="hljs-comment">/* Response example
{
    "response": "C6H12O6 is the molecular formula for glucose. Glucose is a monosaccharide, or simple sugar, that is made from 6 carbons atoms, 12 hydrogen atoms, and 6 oxygen atoms.",
    "documents": [
        {
          "name": "research_paper.pdf",
          "raw_content": "...",
          "content": "...",
          "summary": "...",
          "metadata": "{...}"
        },
        {
          "name": "chemistry_101.pdf",
          "raw_content": "...",
          "content": "...",
          "summary": "...",
          "metadata": "{...}"
        }
    ]    
}
*/</span>
</code></pre>
<h2 id="heading-data-privacy-and-security">Data privacy and security</h2>
<p>At Nearform, data privacy and security are paramount. We implement robust measures to protect sensitive information throughout the entire processing pipeline. To ensure the confidentiality and integrity of data processed by the library, Nearform employs several robust measures:</p>
<ol>
<li><p><strong>Data encryption</strong>: All data, including documents and their summaries, are encrypted both in transit and at rest. This prevents unauthorised access and ensures that data is secure throughout the processing pipeline.</p>
</li>
<li><p><strong>Anonymisation techniques</strong>: When processing documents, any personally identifiable information (PII) is anonymised to protect individual privacy.</p>
</li>
<li><p><strong>Regular audits</strong>: The library undergoes regular security audits to identify and mitigate potential vulnerabilities, ensuring continuous improvement in data protection practices.</p>
</li>
<li><p><strong>Prompt design</strong>: As mentioned, the library uses carefully crafted prompts to prevent hallucinations, which also plays a role in avoiding the generation of sensitive information that isn't present in the source documents.</p>
</li>
</ol>
<p>By integrating these practices, Nearform demonstrates its commitment to safeguarding user data, ensuring that the <strong>pgRAG</strong> library is not only powerful and efficient but also trustworthy and secure.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>By harnessing the transformative power of Generative AI, Nearform is not just empowering businesses – we're revolutionising the way they utilise their data. Our AI accelerator library, <strong>pgRAG</strong>, simplifies the integration process, making this cutting-edge technology accessible to a wider audience. Leveraging state-of-the-art natural language processing (NLP) techniques and large language models (LLMs), <strong>pgRAG</strong> offers powerful text analysis, summarisation, and generation capabilities across various document formats.</p>
<p>Currently, <strong>pgRAG</strong> is an internal tool within Nearform, but we are actively exploring the possibility of releasing it as an open-source project in the future. This would allow the broader developer community to contribute to its development and leverage its capabilities for their own applications.</p>
<p>At Nearform, we are committed to the responsible and ethical use of AI technologies, ensuring that our solutions prioritise data privacy, security and accuracy.</p>
<p>We're excited to see how GenAI technology continues to evolve and transform the way we work. Our team is actively researching and developing new techniques to improve the accuracy, efficiency and scalability of our AI accelerator library.</p>
<p>Stay tuned for more updates from the Nearform team as we continue to explore and push the boundaries of AI technology. Together, we're shaping the future of business innovation.</p>
]]></content:encoded></item><item><title><![CDATA[Improving the accuracy of LLM responses with GraphRAG]]></title><description><![CDATA[By Bilal Shareef Generative AI (GenAI) is emerging as a powerful technology across tech industries. It is used to generate text responses for user prompts…]]></description><link>https://nearform.hashnode.dev/improving-the-accuracy-of-llm-responses-with-graphrag</link><guid isPermaLink="true">https://nearform.hashnode.dev/improving-the-accuracy-of-llm-responses-with-graphrag</guid><category><![CDATA[General Programming]]></category><category><![CDATA[iwritecode]]></category><category><![CDATA[Programming Blogs]]></category><category><![CDATA[llm]]></category><category><![CDATA[RAG ]]></category><dc:creator><![CDATA[Nearform]]></dc:creator><pubDate>Wed, 05 Jun 2024 23:00:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1718358651344/e50fa7d8-f0d4-424c-b1a2-55dbd4ea0e48.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>By Bilal Shareef</p>
<h2 id="heading-we-explore-graphrag-by-implementing-it-in-a-javascript-application">We explore GraphRAG by implementing it in a JavaScript application</h2>
<p>Generative AI (GenAI) is emerging as a powerful technology across tech industries. It is used to generate text responses for user prompts or questions and is based on large language models (LLMs) trained on massive data resources.</p>
<p>Training LLMs is an expensive process, making regular training with up-to-date data impractical. Consequently, LLM knowledge is often outdated. With that being said, a common issue with LLMs is their occasional tendency to provide incorrect responses.</p>
<p><strong>Retrieval-Augmented Generation (RAG)</strong> is an approach that can solve this problem by enabling LLMs to access additional data resources without requiring retraining, resulting in better answers. An even more effective approach is <strong>GraphRAG</strong>, which can provide more accurate answers and is essentially an extension of RAG.</p>
<p>In this blog post, we will explore GraphRAG by implementing it in a JavaScript application. We drew inspiration from a LangChain <a target="_blank" href="https://blog.langchain.dev/enhancing-rag-based-applications-accuracy-by-constructing-and-leveraging-knowledge-graphs/">blog post</a>, which explores GraphRAG with a Python application. LangChain is a framework for building applications based on LLMs. It provides tools and abstractions to improve the customisation, accuracy, and relevancy of the information generated by the models. We will be using <a target="_blank" href="https://www.langchain.com/langchain">LangChain JS</a> framework in the application demonstrated in this blog post.</p>
<h2 id="heading-what-is-rag">What is RAG?</h2>
<p>Before delving into GraphRAG, let's first understand what RAG is. RAG is an AI technique that can enhance the quality of GenAI by enabling LLMs to access additional data without the need for retraining.</p>
<p>It offers a method for optimising the output of an LLM with targeted information without modifying the underlying model itself. This means that the RAG approach in GenAI can provide more contextually appropriate answers based on current data.</p>
<p>Typically, RAG implementations require vector databases that enable rapid storage and retrieval of data fed into the LLM.</p>
<h2 id="heading-what-is-graphrag">What is GraphRAG?</h2>
<p>GraphRAG is an extension of RAG where a graph database is used alongside a vector database to store and retrieve data. Vector databases excel in handling unstructured data, but they cannot efficiently store interconnected and structured information. That's where a graph database comes into play.</p>
<p>The concept behind GraphRAG is to harness the structured nature of data to enrich the depth and contextuality of retrieved information. Graph databases organise data into nodes and relationships, aiding LLMs in better understanding the data and providing more accurate answers.</p>
<p>As GraphRAG extends RAG, we can integrate structured data obtained from the graph database with unstructured data obtained from the vector database and then input it into the LLM. The model can thus utilise both structured and unstructured data to generate improved responses. In our demonstration, we will be using a <a target="_blank" href="https://neo4j.com/">Neo4j</a> database to store both structured and unstructured data.</p>
<p>Now that we understand what RAG and GraphRAG are, let's build an application that demonstrates this.</p>
<p>The application consists of two parts: the loader and the retriever.</p>
<ol>
<li><p>The loader fetches the source content from a knowledge base and stores the data in the Neo4j database.</p>
</li>
<li><p>The retriever fetches data from the Neo4j database and feeds it into the LLM to obtain responses. The retriever runs on demand whenever a question or prompt is sent.</p>
</li>
</ol>
<p>The code for the application explained in this blog post is available <a target="_blank" href="https://github.com/nearform/graph-rag-js-research/">on GitHub</a>.</p>
<h2 id="heading-environment-setup">Environment setup</h2>
<p>In our application, alongside LangChain and Neo4j, we will be using <a target="_blank" href="https://openai.com/">OpenAI</a>. So let’s set these up first.</p>
<p>There are two options for setting up a Neo4j database. The easiest option is to use <a target="_blank" href="https://neo4j.com/cloud/platform/aura-graph-database/">Neo4j Aura,</a> which offers a free instance of the Neo4j database. The other option is to set up a local instance of the Neo4j database by downloading the <a target="_blank" href="https://neo4j.com/docs/desktop-manual/current/">Neo4j Desktop</a> application and creating a local database instance.</p>
<p>Once the Neo4j database instance is ready, we will have the URI, username, and password of the database instance. We also need an OpenAI API key to use their LLM models. Set this data in environment variables so that the application can read it from there.</p>
<pre><code class="lang-js"><span class="hljs-keyword">const</span> url = process.env.NEO4J_URI
<span class="hljs-keyword">const</span> username = process.env.NEO4J_USERNAME
<span class="hljs-keyword">const</span> password = process.env.NEO4J_PASSWORD
<span class="hljs-keyword">const</span> openAIApiKey = process.env.OPENAI_API_KEY
</code></pre>
<h2 id="heading-loader">Loader</h2>
<p>Let's dive into the coding part of the loader.</p>
<p>We will first initialise the LLM by creating an instance of <a target="_blank" href="https://js.langchain.com/docs/integrations/chat/openai">ChatOpenAI</a>. We will pass a couple of parameters to set the temperature and the LLM model along with the OpenAI API key.</p>
<p>For our scenario, where we want the LLM to respond based on the stored knowledge base, there isn’t a need for the LLM to be creative. So let’s set the temperature to 0 to ensure we get deterministic outputs.</p>
<p>There are a bunch of LLMs offered by OpenAI. The <code>gpt-4-turbo</code> model is the most recent version at the time of writing this article. It is trained with data up to December 2023 and is more capable and reliable. However, we will be using <code>gpt-3.5-turbo</code> as it is relatively cheaper and sufficient for our demonstration.</p>
<p>Also, let’s initialise the <a target="_blank" href="https://js.langchain.com/docs/modules/data_connection/experimental/graph_databases/neo4j">Neo4jGraph</a> database.</p>
<pre><code class="lang-js"><span class="hljs-keyword">const</span> llm = <span class="hljs-keyword">new</span> ChatOpenAI({
  <span class="hljs-attr">temperature</span>: <span class="hljs-number">0</span>,
  <span class="hljs-attr">modelName</span>: <span class="hljs-string">'gpt-3.5-turbo'</span>,
  openAIApiKey
})

<span class="hljs-keyword">const</span> graph = <span class="hljs-keyword">await</span> Neo4jGraph.initialize({ url, username, password })
</code></pre>
<p>Then, load the source text content from a knowledge base. In our application, we will use the <a target="_blank" href="https://js.langchain.com/docs/integrations/document_loaders/web_loaders/imsdb">IMSDBLoader</a> provided by LangChain to retrieve text content. LangChain offers a variety of text <a target="_blank" href="https://js.langchain.com/docs/integrations/document_loaders/web_loaders/">loaders</a> that allow us to easily fetch text content from various web resources.</p>
<p>The IMSDBLoader is one such loader provided by LangChain, enabling us to fetch content from <a target="_blank" href="https://imsdb.com/">IMSDb</a>, a website where movie scripts can be found.</p>
<p>We will retrieve data from just one page (the script of the movie <em>The Avengers</em>) on IMSDB. The <code>load</code> method of the loader will return an array of documents (one document per page). Since we are loading only one page in our example below, the length of the returned array will be 1.</p>
<pre><code class="lang-js"><span class="hljs-keyword">const</span> loader = <span class="hljs-keyword">new</span> IMSDBLoader(
  <span class="hljs-string">'&lt;https://imsdb.com/scripts/Avengers,-The-(2012).html&gt;'</span>
)
<span class="hljs-keyword">const</span> rawDocs = <span class="hljs-keyword">await</span> loader.load()
</code></pre>
<p>Now that we have the raw text content loaded, let’s split it into multiple smaller chunks. LangChain provides several <a target="_blank" href="https://js.langchain.com/docs/modules/data_connection/document_transformers/">text splitters</a> that enable us to divide long documents into smaller segments. We will utilise the <a target="_blank" href="https://js.langchain.com/docs/modules/data_connection/document_transformers/token_splitter">TokenTextSplitter</a> utility class for chunking, specifying only the chunk size and the chunk overlap size in bytes.</p>
<p>Next, we will iterate over the returned list of chunks and create a new list with <a target="_blank" href="https://api.js.langchain.com/classes/langchain_core_documents.Document.html">Document</a> objects for each chunk.</p>
<pre><code class="lang-js"><span class="hljs-keyword">const</span> textSplitter = <span class="hljs-keyword">new</span> TokenTextSplitter({
  <span class="hljs-attr">chunkSize</span>: <span class="hljs-number">512</span>,
  <span class="hljs-attr">chunkOverlap</span>: <span class="hljs-number">24</span>
})

<span class="hljs-keyword">let</span> documents = []
<span class="hljs-keyword">for</span> (<span class="hljs-keyword">let</span> i = <span class="hljs-number">0</span>; i &lt; rawDocs.length; i++) {
  <span class="hljs-keyword">const</span> chunks = <span class="hljs-keyword">await</span> textSplitter.splitText(rawDocs[i].pageContent)
  <span class="hljs-keyword">const</span> processedDocs = chunks.map(
    <span class="hljs-function">(<span class="hljs-params">chunk, index</span>) =&gt;</span>
      <span class="hljs-keyword">new</span> Document({
        <span class="hljs-attr">pageContent</span>: chunk,
        <span class="hljs-attr">metadata</span>: {
          <span class="hljs-attr">a</span>: index + <span class="hljs-number">1</span>,
          ...rawDocs[i].metadata
        }
      })
  )
  documents.push(...processedDocs)
}
</code></pre>
<p>The next step is to convert the list of documents into a list of graph documents. LangChain offers an <a target="_blank" href="https://js.langchain.com/docs/use_cases/graph/construction#llm-graph-transformer">LLMGraphTransformer</a> class, which performs this conversion for us. Graph Documents comprise a blend of structured and unstructured data. Structured data is represented as nodes and relationships, while unstructured data is the actual source text content.</p>
<p>LLMGraphTransformer utilises the LLM to determine the nodes and relationships for a given text document.</p>
<pre><code class="lang-js"><span class="hljs-keyword">const</span> llmTransformer = <span class="hljs-keyword">new</span> LLMGraphTransformer({ llm })
<span class="hljs-keyword">const</span> graphDocuments = <span class="hljs-keyword">await</span> llmTransformer.convertToGraphDocuments(documents)
</code></pre>
<p>Save the Graph Documents to the Neo4j database using the <code>addGraphDocuments</code> method.</p>
<pre><code class="lang-js"><span class="hljs-keyword">await</span> graph.addGraphDocuments(graphDocuments, {
  <span class="hljs-attr">baseEntityLabel</span>: <span class="hljs-literal">true</span>,
  <span class="hljs-attr">includeSource</span>: <span class="hljs-literal">true</span>
})
</code></pre>
<p>The loader part is pretty simple, as you can see above. We loaded the source content from the IMSDB loader and then split the content into smaller chunks. Next, we converted the text chunks into graph documents with the help of LLMGraphTransformer and stored them in the Neo4j database.</p>
<p>Now that the loader code is ready, give it a try by running it. You should see the graph documents for the passed source content saved successfully in the Neo4j database.</p>
<h2 id="heading-retriever">Retriever</h2>
<p>Let's dive into the coding part of the retriever. We will start by initialising the LLM and the Neo4jGraph database, just as we did in the loader.</p>
<p>Next, we need to initialise the Neo4j vector store, which we will use to fetch the unstructured data stored in the loader part. We will accomplish this by invoking the <code>fromExistingGraph</code> static method of <a target="_blank" href="https://js.langchain.com/docs/integrations/vectorstores/neo4jvector#instantiate-neo4jvectorstore-from-existing-graph">Neo4jVectorStore</a>, which returns a handle to the database. We will use this handle later in the application to fetch the unstructured text data.</p>
<pre><code class="lang-js"><span class="hljs-keyword">const</span> neo4jVectorIndex = <span class="hljs-keyword">await</span> Neo4jVectorStore.fromExistingGraph(
  <span class="hljs-keyword">new</span> OpenAIEmbeddings({ openAIApiKey }),
  {
    url,
    username,
    password,
    <span class="hljs-attr">searchType</span>: <span class="hljs-string">'hybrid'</span>,
    <span class="hljs-attr">nodeLabel</span>: <span class="hljs-string">'Document'</span>,
    <span class="hljs-attr">textNodeProperties</span>: [<span class="hljs-string">'text'</span>],
    <span class="hljs-attr">embeddingNodeProperty</span>: <span class="hljs-string">'embedding'</span>
  }
)
</code></pre>
<p>The next step is to define a chain that enables us to extract entities from a given string or question.</p>
<p>In LangChain, chains represent sequences of calls — whether to an LLM, a tool, or a data preprocessing step. In our application, we will use chains to deliver prompt templates and inputs to the LLM to obtain generated data.</p>
<p>An entity is essentially an object or thing that exists in the real world. For example, in the question "<strong>Who is Tony Stark?</strong>", "<strong>Tony Stark</strong>" is an entity. There could be more than one entity in a given string as well. For example, in the question "<strong>How are Thor and Loki related?</strong>", "<strong>Thor</strong>" and "<strong>Loki</strong>" are entities.</p>
<p>In the following code, we are defining a prompt template that instructs the LLM to extract entities from the passed question. Additionally, we are defining the output schema, which specifies the structure of the output we require — a list of entities.</p>
<p>The entity chain is created by passing the prompt, output schema, and the LLM to the <code>createStructuredOutputRunnable</code> function. When the entity chain is invoked, the prompt and the output schema are fed to the LLM, which extracts and returns the entities.</p>
<pre><code class="lang-js"><span class="hljs-keyword">const</span> entitiesSchema = z
  .object({
    <span class="hljs-attr">names</span>: z
      .array(z.string())
      .describe(
        <span class="hljs-string">'All the person, organization, or business entities that appear in the text'</span>
      )
  })
  .describe(<span class="hljs-string">'Identifying information about entities.'</span>)

<span class="hljs-keyword">const</span> prompt = ChatPromptTemplate.fromMessages([
  [
    <span class="hljs-string">'system'</span>,
    <span class="hljs-string">'You are extracting organization and person entities from the text.'</span>
  ],
  [
    <span class="hljs-string">'human'</span>,
    <span class="hljs-string">'Use the given format to extract information from the following input: {question}'</span>
  ]
])

<span class="hljs-keyword">const</span> entityChain = createStructuredOutputRunnable({
  <span class="hljs-attr">outputSchema</span>: entitiesSchema,
  prompt,
  llm
})
</code></pre>
<p>Next, let's execute a Neo4j query to create a <a target="_blank" href="https://neo4j.com/docs/cypher-manual/current/indexes/semantic-indexes/full-text-indexes/">full-text index</a> — if it doesn’t already exist. A full-text index is utilised to index nodes and relationships, enabling querying of nodes later in the application.</p>
<p>This query will create the index when you run the retriever for the first time. It won’t perform any actions in subsequent runs.</p>
<pre><code class="lang-js"><span class="hljs-keyword">await</span> graph.query(
  <span class="hljs-string">'CREATE FULLTEXT INDEX entity IF NOT EXISTS FOR (e:__Entity__) ON EACH [e.id]'</span>
)
</code></pre>
<p>Then, let’s define a structured retriever function that invokes the entity chain to extract entities from the question. For each entity, it executes a query to fetch matching nodes and relationships from the Neo4j database.</p>
<pre><code class="lang-js"><span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">structuredRetriever</span>(<span class="hljs-params">question</span>) </span>{
  <span class="hljs-keyword">let</span> result = <span class="hljs-string">''</span>
  <span class="hljs-keyword">const</span> entities = <span class="hljs-keyword">await</span> entityChain.invoke({ question })

  <span class="hljs-keyword">for</span> (<span class="hljs-keyword">const</span> entity <span class="hljs-keyword">of</span> entities.names) {
    <span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> graph.query(
      <span class="hljs-string">`CALL db.index.fulltext.queryNodes('entity', $query, {limit:2})
      YIELD node,score
      CALL {
        MATCH (node)-[r:!MENTIONS]-&gt;(neighbor)
        RETURN node.id + ' - ' + type(r) + ' -&gt; ' + neighbor.id AS
        output
        UNION
        MATCH (node)&lt;-[r:!MENTIONS]-(neighbor)
        RETURN neighbor.id + ' - ' + type(r) + ' -&gt; ' +  node.id AS
        output
      }
      RETURN output LIMIT 50`</span>,
      { <span class="hljs-attr">query</span>: generateFullTextQuery(entity) }
    )

    result += response.map(<span class="hljs-function"><span class="hljs-params">el</span> =&gt;</span> el.output).join(<span class="hljs-string">'\\n'</span>) + <span class="hljs-string">'\\n'</span>
  }

  <span class="hljs-keyword">return</span> result
}
</code></pre>
<p>Now we can define the final retriever function. It takes the question as input and then retrieves the structured and unstructured data for the passed question. The structured data is fetched from the <code>structuredRetriever</code> we have defined above, and the unstructured data is retrieved from the vector index.</p>
<pre><code class="lang-js"><span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">retriever</span>(<span class="hljs-params">question</span>) </span>{
  <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Standalone Question - '</span> + question)
  <span class="hljs-keyword">const</span> structuredData = <span class="hljs-keyword">await</span> structuredRetriever(question)

  <span class="hljs-keyword">const</span> similaritySearchResults =
    <span class="hljs-keyword">await</span> neo4jVectorIndex.similaritySearch(question)
  <span class="hljs-keyword">const</span> unstructuredData = similaritySearchResults.map(<span class="hljs-function"><span class="hljs-params">el</span> =&gt;</span> el.pageContent)

  <span class="hljs-keyword">const</span> finalData = <span class="hljs-string">`Structured data:
  <span class="hljs-subst">${structuredData}</span>
  Unstructured data:
  <span class="hljs-subst">${unstructuredData.map(content =&gt; <span class="hljs-string">`#Document <span class="hljs-subst">${content}</span>`</span>).join(<span class="hljs-string">'\\n'</span>)}</span>
      `</span>
  <span class="hljs-keyword">return</span> finalData
}
</code></pre>
<p>The code we have written so far covers data retrieval from the database for a given question. Now let’s implement a few chains and link them together to make the entire process work.</p>
<p>In our application, we aim to maintain the context of the conversation to ensure continuity between questions. For instance, a user might ask, "<strong>Who is Tony Stark?</strong>" and then follow up with, "<strong>Does he own the Stark Tower?</strong>" In this scenario, the LLM needs to comprehend who "<strong>he</strong>" refers to in the context of the conversation to generate the correct response.</p>
<p>We will define a standalone chain that can understand the context based on the conversation history and then return a new question with the ambiguous parts resolved. In the example provided, when "<strong>Does he own the Stark Tower?</strong>" is asked, the chain will determine the context with the assistance of the conversation history and return a standalone question: "<strong>Does Tony Stark own the Stark Tower?</strong>".</p>
<pre><code class="lang-js"><span class="hljs-keyword">const</span> standaloneTemplate = <span class="hljs-string">`Given the following conversation and a follow up question, rephrase the follow up question to be a standalone question.
Chat History:
{conversationHistory}
Follow Up Input: {question}
Standalone question:`</span>

<span class="hljs-keyword">const</span> standalonePrompt = PromptTemplate.fromTemplate(standaloneTemplate)

<span class="hljs-keyword">const</span> standaloneQuestionChain = standalonePrompt
  .pipe(llm)
  .pipe(<span class="hljs-keyword">new</span> StringOutputParser())
</code></pre>
<p>Then, we will define a retriever chain that retrieves context (structured and unstructured data) from the database for the generated standalone question by invoking the retriever function we implemented above.</p>
<pre><code class="lang-js"><span class="hljs-keyword">const</span> retrieverChain = RunnableSequence.from([
  <span class="hljs-function"><span class="hljs-params">prevResult</span> =&gt;</span> prevResult.standaloneQuestion,
  retriever
])
</code></pre>
<p>Now, let’s define a chain that can generate the final response based on the context and the original question.</p>
<pre><code class="lang-js"><span class="hljs-keyword">const</span> answerTemplate = <span class="hljs-string">`You are a helpful and enthusiastic support bot who can answer any question based on the context provided and the conversation history. Try to find the answer in the context. If the answer is not given in the context, find the answer in the conversation history if possible. If you really don't know the answer, say "I am sorry, I don't know the answer to that.". And don't try to makeup the answer. Always speak as you are chatting to a friend

context:{context}
question: {question}
answer:`</span>

<span class="hljs-keyword">const</span> answerPrompt = PromptTemplate.fromTemplate(answerTemplate)

<span class="hljs-keyword">const</span> answerChain = answerPrompt.pipe(llm).pipe(<span class="hljs-keyword">new</span> StringOutputParser())
</code></pre>
<p>Finally, let’s connect the above three chains into a single sequential chain. The idea is to first invoke the standalone chain to obtain the standalone question, then invoke the retriever chain to acquire the context, and finally feed the question and context to the answer chain to generate the final response.</p>
<pre><code class="lang-js"><span class="hljs-keyword">const</span> chain = RunnableSequence.from([
  {
    <span class="hljs-attr">standaloneQuestion</span>: standaloneQuestionChain,
    <span class="hljs-attr">orignalInput</span>: <span class="hljs-keyword">new</span> RunnablePassthrough()
  },
  {
    <span class="hljs-attr">context</span>: retrieverChain,
    <span class="hljs-attr">question</span>: <span class="hljs-function">(<span class="hljs-params">{ orignalInput }</span>) =&gt;</span> orignalInput.question,
    <span class="hljs-attr">conversationHistory</span>: <span class="hljs-function">(<span class="hljs-params">{ orignalInput }</span>) =&gt;</span> orignalInput.conversationHistory
  },
  answerChain
])
</code></pre>
<p>Now let's ask our questions one after the other by invoking the final chain. Additionally, we can capture each of the responses into an array to maintain the conversation history.</p>
<pre><code class="lang-js"><span class="hljs-keyword">const</span> conversationHistory = []

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">logResult</span>(<span class="hljs-params">result</span>) </span>{
  <span class="hljs-built_in">console</span>.log(<span class="hljs-string">`Search Result - <span class="hljs-subst">${result}</span>\\n`</span>)
}

<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">ask</span>(<span class="hljs-params">question</span>) </span>{
  <span class="hljs-built_in">console</span>.log(<span class="hljs-string">`Search Query - <span class="hljs-subst">${question}</span>`</span>)
  <span class="hljs-keyword">const</span> answer = <span class="hljs-keyword">await</span> chain.invoke({
    question,
    <span class="hljs-attr">conversationHistory</span>: formatChatHistory(conversationHistory)
  })
  conversationHistory.push(question)
  conversationHistory.push(answer)
  logResult(answer)
}

<span class="hljs-keyword">await</span> ask(<span class="hljs-string">'Loki is a native of which planet?'</span>)
<span class="hljs-keyword">await</span> ask(<span class="hljs-string">'What is the name of his brother?'</span>)
<span class="hljs-keyword">await</span> ask(<span class="hljs-string">'Who is the villain among the two?'</span>)
<span class="hljs-keyword">await</span> ask(<span class="hljs-string">'Who is Tony Stark?'</span>)
<span class="hljs-keyword">await</span> ask(<span class="hljs-string">'Does he own the Stark Tower?'</span>)
</code></pre>
<p>Below are the answers returned by the LLM for each of the questions asked.</p>
<pre><code class="lang-text">Search Query - Loki is a native of which planet?
Standalone Question - What planet is Loki a native of?
Search Result - Loki is a native of Asgard.

Search Query - What is the name of his brother?
Standalone Question - What is the name of Loki's brother?
Search Result - His brother's name is Thor.

Search Query - Who is the villain among the two?
Standalone Question - Which of the two, Loki and Thor, is considered the villain?
Search Result - The villain is Loki.

Search Query - Who is Tony Stark?
Standalone Question - Who is Tony Stark?
Search Result - Tony Stark is a character who is also known as Iron Man. He is a genius engineer, industrialist, and philanthropist who uses his suit of armor to fight crime and protect the world.

Search Query - Does he own the Stark Tower?
Standalone Question - Does Tony Stark own the Stark Tower?
Search Result - Yes, Tony Stark owns the Stark Tower.
</code></pre>
<p>As you can see, our application was able to generate standalone questions based on the context of the conversation and also returned the correct responses for each of them.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>To summarise what we have covered in this blog post, we implemented a loader module that fetches raw text data from a web source, converts it into graph documents, and stores it in the Graph database. Next, we implemented a retriever module that takes a user's question, extracts entities from it, retrieves both structured and unstructured data related to the entities and the question, and feeds this data to the LLM model to generate an answer.</p>
<p>GraphRAG greatly benefits businesses by improving customer service through precise and relevant interactions. For industries relying on extensive knowledge bases, the ability to generate accurate information can streamline processes, reduce errors, and enhance decision-making. Additionally, the adaptive nature of GraphRAG allows businesses to tailor responses based on specific contextual needs, providing a more personalised and efficient user experience. Ultimately, the integration of GraphRAG can give businesses a competitive advantage in an ever-evolving digital landscape.</p>
<h2 id="heading-references">References</h2>
<p>Here are some interesting blog posts that will help expand your understanding of GraphRAG:</p>
<ul>
<li><p><a target="_blank" href="https://www.microsoft.com/en-us/research/blog/graphrag-unlocking-llm-discovery-on-narrative-private-data/">GraphRAG: Unlocking LLM discovery on narrative private data</a></p>
</li>
<li><p><a target="_blank" href="https://neo4j.com/blog/neo4j-microsoft-collaboration/">Neo4j Integrates with Microsoft to Evolve Data Analytics and Enhance GenAI</a></p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Milo: A new HTTP parser for Node.js]]></title><description><![CDATA[By Paolo Insogna The Hypertext Transfer Protocol (HTTP) stands as one of the oldest and most critical protocols in daily internet usage. Ensuring a solid…]]></description><link>https://nearform.hashnode.dev/milo-a-new-http-parser-for-nodejs</link><guid isPermaLink="true">https://nearform.hashnode.dev/milo-a-new-http-parser-for-nodejs</guid><category><![CDATA[General Programming]]></category><category><![CDATA[iwritecode]]></category><category><![CDATA[Programming Blogs]]></category><category><![CDATA[http]]></category><category><![CDATA[Node.js]]></category><dc:creator><![CDATA[Nearform]]></dc:creator><pubDate>Wed, 29 May 2024 23:00:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1718357086465/47f25a4a-baa9-45ef-9770-d21709318ff8.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>By Paolo Insogna</p>
<h2 id="heading-as-nodejs-continues-to-evolve-milo-stands-ready-to-facilitate-seamless-http-communication">As Node.js continues to evolve, Milo stands ready to facilitate seamless HTTP communication</h2>
<p><strong>The Hypertext Transfer Protocol (HTTP) stands as one of the oldest and most critical protocols in daily internet usage. Ensuring a solid and performant implementation of this protocol is fundamental for both user satisfaction and developer efficiency.</strong></p>
<p>Let's explore how Node.js tackles this task and introduces Milo, a promising replacement for the current HTTP parser, <a target="_blank" href="https://llhttp.org/">llhttp</a>.</p>
<h2 id="heading-a-journey-through-http-versions">A journey through HTTP versions</h2>
<p>Originating in 1991 with HTTP/0.9, the HTTP protocol has evolved over the years. Despite its age, only three main versions exist today:</p>
<ol>
<li><p><strong>HTTP/1.1</strong>: The most widely used version, defined in <a target="_blank" href="https://www.rfc-editor.org/rfc/rfc9112.html">RFC 9112</a>.</p>
</li>
<li><p><strong>HTTP/2.0</strong>: An evolution built upon SPDY, aiming to address TCP protocol limitations, detailed in <a target="_blank" href="https://www.rfc-editor.org/rfc/rfc9113.html">RFC 9113</a>.</p>
</li>
<li><p><strong>HTTP/3.0</strong>: The latest iteration, based on QUIC, ushering HTTP over UDP for the first time. Defined in <a target="_blank" href="https://www.rfc-editor.org/rfc/rfc9114.html">RFC 9114.</a></p>
</li>
</ol>
<p>While HTTP/0.9 and HTTP/1.0 remain widely used, they never achieved RFC standardisation and are now considered obsolete. The current version of the procotols instead share common semantics in <a target="_blank" href="https://www.rfc-editor.org/rfc/rfc9110.html">RFC 9110</a>.</p>
<p>Node.js currently supports HTTP/1.1 and, to some extent, HTTP/0.9 and HTTP/1.0 via its <code>http</code> and <code>https</code> modules. Although HTTP/2.0 is supported through the <code>http2</code> module, active development is halted due to the relatively low adoption rate and the emergence of HTTP/3.0.</p>
<h2 id="heading-the-state-of-http-parsing-in-nodejs">The state of HTTP parsing in Node.js</h2>
<p>Presently, Node.js employs the <a target="_blank" href="https://llhttp.org/">llhttp</a> parser, replacing its predecessor, <a target="_blank" href="https://github.com/nodejs/http-parser">http_parser</a>, due to performance and maintainability issues. Developed by <a target="_blank" href="https://github.com/indutny">Fedor Indutny</a> in 2019, llhttp operates by specifying parsing rules in TypeScript, transpiling them into a high-performance parser in C using <a target="_blank" href="https://llparse.org/">llparse</a>. In the image below you can see a visual representation of llhttp state machine.</p>
<p><img src="https://www.nearform.com/media/pages/digital-community/milo-a-new-http-parser-for-node-js/85e4f4c899-1716894152/blog-milo-a-new-http-parser-for-node.js-1600x.webp" alt="A visual representation of llhttp state machine" /></p>
<p>llhttp is the default Node.js parser since version 12.0.0. Additionally, llhttp has an extensive test suite, partially inherited by http_parser, which is executed by transpiling Markdown files to C and then execute them.</p>
<p>Despite its efficiency, llhttp suffers from a lack of documentation of its unique architecture, posing challenges for maintenance, evolution and security.</p>
<h2 id="heading-introducing-milo-a-modern-http-parser">Introducing Milo: A modern HTTP parser</h2>
<p>In response to the limitations of llhttp, <a target="_blank" href="https://github.com/ShogunPanda">Paolo Insogna</a> started <a target="_blank" href="https://github.com/ShogunPanda/milo">Milo</a> in 2023 — a new HTTP parser written in Rust. Milo retains a state-based architecture akin to llhttp but leverages Rust's powerful <a target="_blank" href="https://doc.rust-lang.org/reference/procedural-macros.html">procedural macros</a> to define states, resulting in more readable and maintainable code.</p>
<p>For instance, the snippet below is an <a target="_blank" href="https://github.com/ShogunPanda/milo/blob/156f2282659578fcb06b8c7cc1fe10d6388fb5c3/parser/src/states.rs#L112">actual Milo state definition</a>.</p>
<pre><code class="lang-text">state!(request_protocol, {
  match data {
    string!("HTTP/") | string!("RTSP/") =&gt; {
      callback!(on_protocol, 4);
      parser.position += 4;

      move_to!(request_version, 1)
    }
    otherwise!(5) =&gt;
      fail!(UNEXPECTED_CHARACTER, "Expected protocol"),
    _ =&gt; suspend!(),
  }
});
</code></pre>
<p><em>The programming language above is Rust.</em></p>
<p>When waiting for the protocol, which follows the URL in first line of the request (example: <code>GET /home HTTP/1.1</code>), the parser will try to match the current data with three possible cases:</p>
<ol>
<li><p>If it starts with <code>HTTP/</code> or <code>RTSP/</code> , then it will invoke the <code>on_protocol</code> callback, will advance the position in the data by 4 characters and then move the parser to the <code>request_version</code> state.</p>
</li>
<li><p>Otherwise, if at least 5 characters are available then it will mark the parsing as failed.</p>
</li>
<li><p>Otherwise, if not enough characters are available (<code>_ =&gt; ...</code>) it will suspend the parsing.</p>
</li>
</ol>
<p>And below is the actual compiled Rust code after the macros have been replaced (which happens at compile time, so no performance penalty).</p>
<pre><code class="lang-text">// This is part of a big "match" statement
STATE_REQUEST_PROTOCOL =&gt; {
  match data {
      [72u8,
      84u8,
      84u8,
      80u8,
      47u8,
      ..,
      ]
      | [82u8,
      84u8,
      83u8,
      80u8,
      47u8,
      ..,
      ] =&gt; {
          #[cfg(not(target_family = "wasm"))]
          (self.callbacks.on_protocol)(self, self.position, 4);
          self.position += 4;
          self.state = STATE_REQUEST_VERSION;
          self.position += 1;
      }
      [_u0, _u1, _u2, _u3, _u4, ..] =&gt; {
          self.fail(ERROR_UNEXPECTED_CHARACTER, "Expected protocol");
          break 'parser;
      }
      _ =&gt; {
          not_suspended = false;
          break 'state;
      }
  }
}
</code></pre>
<p><em>The programming language above is Rust.</em></p>
<p>Even with limited Rust knowledge, it's relatively simple to map statements between the two snippets.</p>
<p>Using the macros and an eager approach, Milo was able to reduce the number of states to around 30 (while llhttp has around 80).</p>
<p>Unlike llhttp, Milo strictly parses only HTTP/1.1, reducing complexity and chances of introducing vulnerabilities.</p>
<h2 id="heading-how-to-utilise-milo">How to utilise Milo</h2>
<p>Milo offers three implementations: Rust, native (C++, via <a target="_blank" href="https://github.com/mozilla/cbindgen">cbindgen</a>), and WebAssembly (JavaScript). While the Rust implementation is straightforward, the native and WebAssembly versions provide flexibility across different platforms, ensuring compatibility with Node.js environments.</p>
<p>Regardless of the language, Milo ensures a consistent developer experience. The code below shows a sample parsing procedure in Rust with Milo.</p>
<pre><code class="lang-text">use core::ffi::c_void;
use core::slice;
use milo::Parser;

fn main() {
  // Create the parser.
  let mut parser = Parser::new();

  // Prepare a message to parse.
  let message = String::from("HTTP/1.1 200 OK\r\nContent-Length: 3\r\n\r\nabc");
  parser.context = message.as_ptr() as *mut c_void;

  // Milo works using callbacks, All callbacks have the same signature.
  parser.callbacks.on_data = |p: &amp;mut Parser, from: usize, size: usize| {
    let message = unsafe {
      std::str::from_utf8_unchecked(slice::from_raw_parts(p.context.add(from) as *const u8, size))
    };

    // Do something with the informations.
    println!("Pos={} Body: {}", p.position, message);
  };

  // Now perform the main parsing using milo.parse.
  parser.parse(message.as_ptr(), message.len());
}
</code></pre>
<p><em>The programming language above is Rust.</em></p>
<p>When executed via cargo run, it outputs the following:</p>
<pre><code class="lang-text">Pos=38 Body: abc
</code></pre>
<p>The code snippet below shows its equivalents in C++:</p>
<pre><code class="lang-text">#include "milo.h"
#include "stdio.h"
#include "string.h"

int main() {
  // Create the parser.
  milo::Parser* parser = milo::milo_create();

  // Prepare a message to parse.
  const char* message = "HTTP/1.1 200 OK\r\nContent-Length: 3\r\n\r\nabc";
  parser-&gt;context = (char*) message;

  // Milo works using callbacks, All callbacks have the same signature.
  parser-&gt;callbacks.on_data = [](milo::Parser* p, uintptr_t from, uintptr_t size) {
    char* payload = reinterpret_cast&lt;char*&gt;(malloc(sizeof(char) * size));
    strncpy(payload, reinterpret_cast&lt;const char*&gt;(p-&gt;context) + from, size);
    printf("Pos=%lu Body: %s\n", p-&gt;position, payload);
    free(payload);
  };

  // Now perform the main parsing using milo.parse. 
  // The method returns the number of consumed characters.
  milo::milo_parse(parser, reinterpret_cast&lt;const unsigned char*&gt;(message), strlen(message));

  // Cleanup used resources.
  milo::milo_destroy(parser);
}
</code></pre>
<p><em>The programming language above is C++.</em></p>
<p>And JavaScript:</p>
<pre><code class="lang-js"><span class="hljs-keyword">import</span> { milo } <span class="hljs-keyword">from</span> <span class="hljs-string">"@perseveranza-pets/milo"</span>;

<span class="hljs-comment">// Prepare a message to parse.</span>
<span class="hljs-keyword">const</span> message = Buffer.from(<span class="hljs-string">"HTTP/1.1 200 OK\r\nContent-Length: 3\r\n\r\nabc"</span>);

<span class="hljs-comment">// Allocate a memory in the WebAssembly space. </span>
<span class="hljs-comment">// This speeds up data copying to the WebAssembly layer.</span>
<span class="hljs-keyword">const</span> ptr = milo.alloc(message.length);

<span class="hljs-comment">// Create a buffer we can use normally.</span>
<span class="hljs-keyword">const</span> buffer = Buffer.from(milo.memory.buffer, ptr, message.length);

<span class="hljs-comment">// Create the parser.</span>
<span class="hljs-keyword">const</span> parser = milo.create();

<span class="hljs-comment">// Milo works using callbacks, All callbacks have the same signature.</span>
milo.setOnData(parser, <span class="hljs-function">(<span class="hljs-params">p, <span class="hljs-keyword">from</span>, size</span>) =&gt;</span> {
  <span class="hljs-built_in">console</span>.log(
    <span class="hljs-string">`Pos=<span class="hljs-subst">${milo.getPosition(p)}</span> Body: <span class="hljs-subst">${message
      .slice(<span class="hljs-keyword">from</span>, <span class="hljs-keyword">from</span> + size)
      .toString()}</span>`</span>
  );
});

<span class="hljs-comment">// Now perform the main parsing using milo.parse. </span>
<span class="hljs-comment">// The method returns the number of consumed characters.</span>
buffer.set(Buffer.from(message), <span class="hljs-number">0</span>);
<span class="hljs-keyword">const</span> consumed = milo.parse(parser, ptr, message.length);

<span class="hljs-comment">// Cleanup used resources.</span>
milo.destroy(parser);
milo.dealloc(ptr, message.length);
</code></pre>
<h2 id="heading-what-lies-ahead-for-milo-and-nodejs">What lies ahead for Milo and Node.js?</h2>
<p>Milo is currently feature-complete, boasting superior performance in its native version. However, optimisation efforts are underway for the WebAssembly version, which is the one targeted for Node.js integration. Once performance issues are resolved, Milo aims to replace llhttp, bringing Node.js in a new era of HTTP parsing characterised by robustness, performance, and maintainability. As Node.js continues to evolve, Milo stands ready to facilitate seamless HTTP communication, empowering developers to build efficient and reliable web applications.</p>
]]></content:encoded></item><item><title><![CDATA[Real-time data replication in Postgres and Node.js]]></title><description><![CDATA[By Manuel Spigolon In today's fast-paced digital landscape, businesses rely heavily on data to drive decision-making processes. Real-time data replication…]]></description><link>https://nearform.hashnode.dev/real-time-data-replication-in-postgres-and-nodejs</link><guid isPermaLink="true">https://nearform.hashnode.dev/real-time-data-replication-in-postgres-and-nodejs</guid><category><![CDATA[iwritecode]]></category><category><![CDATA[Programming Blogs]]></category><category><![CDATA[General Programming]]></category><category><![CDATA[Node.js]]></category><category><![CDATA[PostgreSQL]]></category><dc:creator><![CDATA[Nearform]]></dc:creator><pubDate>Wed, 22 May 2024 23:00:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1718356714383/73de2993-f4a4-4fd8-9118-eb95411f7c8d.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>By Manuel Spigolon</p>
<h2 id="heading-understand-how-to-do-a-real-time-data-replication-in-postgres-with-nodejs">Understand how to do a real-time data replication in Postgres with Node.js</h2>
<p><strong>In today's fast-paced digital landscape, businesses rely heavily on data to drive decision-making processes. Real-time data replication has emerged as a critical capability for organisations seeking to stay ahead of the curve, enabling them to synchronise data across multiple databases seamlessly.</strong></p>
<p>To make a few practical examples, real-time data replication is essential for scenarios such as:</p>
<ul>
<li><p>Keeping different databases in sync, which is a typical scenario when a company acquires another company and needs to merge the data.</p>
</li>
<li><p>Relying on a database to trigger events in other systems; for example, when a new user is created in the database, a welcome email is sent without modifying existing code or introducing new architecture components like message queues.</p>
</li>
<li><p>Keeping an application's in-memory cache in sync with the database to improve the performance of read queries.</p>
</li>
<li><p>For high availability and disaster recovery scenarios.</p>
</li>
</ul>
<p>As you can see, real-time data replication is a powerful tool that can be used in various scenarios to improve the performance, reliability and scalability of your applications.</p>
<p>In this blog post, we'll explore the power of Postgres replication management, to understand how to do a real-time data replication in Postgres with Node.js.</p>
<p><em>You will need to run some Docker commands to set up a Postgres instance with logical replication enabled, but it is a copy-paste operation.</em></p>
<p><strong>Editor’s note:</strong> This is a cross-post written by Senior Software Developer, Manuel Spigolon. Manuel has his own blog at <a target="_blank" href="https://backend.cafe/">backend.cafe</a> where you can subscribe for updates and find more great posts. Some of the links in the article point to Manuel’s personal GitHub account.</p>
<h2 id="heading-introduction-to-postgres-replication-management">Introduction to Postgres replication management</h2>
<p>PostgreSQL is a powerful open-source relational database system known for its robust features and extensibility. One of its standout capabilities is <strong>replication management</strong>, allowing you to replicate data from one database to another in real-time. This feature is invaluable to organisations that require characteristics such as:</p>
<ul>
<li><p>High availability: the primary database fails, and a standby server takes over seamlessly.</p>
</li>
<li><p>Load balancing: distributing read queries across multiple replicas to improve performance.</p>
</li>
<li><p>Data distribution: synchronising data across geographically distributed systems.</p>
</li>
</ul>
<p>Postgres offers various mechanisms to support the listed scenarios where each solution has its strengths and use cases. To list a few:</p>
<ul>
<li><p>Synchronous multimaster replication: a query must be committed on all nodes before returning to the client. Very high availability and data consistency but with a performance cost.</p>
</li>
<li><p>Write-ahead Log Shipping (WAL): the primary server sends WAL segments to standby servers, which replay them to stay in sync. It's a simple and reliable solution but with a potential for data loss.</p>
</li>
<li><p>Logical Replication: replicates individual changes (inserts, updates, deletes) rather than entire WAL segments. It's suitable for scenarios such as data warehousing, data distribution and selective replication. Moreover, it allows for more granular control over which tables and changes are replicated and it is possible to replicate data between different versions of Postgres or multiple sources!</p>
</li>
</ul>
<p>The complete list of replication mechanisms can be found in the <a target="_blank" href="https://www.postgresql.org/docs/16/different-replication-solutions.html">Postgres documentation</a>.</p>
<p>In this blog post, we'll focus on <a target="_blank" href="https://www.postgresql.org/docs/16/logical-replication.html">Logical Replication</a> mechanism.</p>
<h3 id="heading-the-wal-file"><strong>The WAL file</strong></h3>
<p>The Write-ahead Log (WAL) is a fundamental concept in Postgres replication and we mentioned it in the previous section. The WAL is a file that stores all changes made to the database, such as inserts, updates and deletes, in a sequential manner. We could say that the WAL is our source of truth, and this file is handled by Postgres to guarantee data consistency and <a target="_blank" href="https://www.postgresql.org/docs/16/wal-reliability.html">durability</a> in case of a crash or disk failure.</p>
<p>We must discuss the WAL because it is the foundation of Postgres replication. We can configure Postgres to handle the WAL file, and we can use it to replicate data across multiple databases. So we must understand the <code>wal_level</code> configuration parameter. This parameter defines how much information is written to the WAL file. The possible values are:</p>
<ul>
<li><p><code>minimal</code>: only the information needed to recover from a crash is written to the WAL file.</p>
</li>
<li><p><code>replica</code>: the default value, it writes enough information to support WAL archiving and replication.</p>
</li>
<li><p><code>logical</code>: it stores the same information as <code>replica</code> and additional information needed for logical replication, so this configuration requires more disk space.</p>
</li>
</ul>
<h2 id="heading-developing-a-real-time-data-replication-system-with-nodejs">Developing a real-time data replication system with Node.js</h2>
<p>To develop a real-time data replication system with Node.js, we must follow these steps:</p>
<ol>
<li><p>Initialise the Postgres database.</p>
</li>
<li><p>Create a data producer we want to replicate.</p>
</li>
<li><p>Create a data consumer that replicates the data through the Postgres replication mechanism.</p>
</li>
</ol>
<p>So let's start!</p>
<h3 id="heading-setting-up-postgres"><strong>Setting up Postgres</strong></h3>
<p>We can start our journey into Postgres replication management by starting a Postgres instance and configuring it to support logical replication. Let's use Docker for this task:</p>
<pre><code class="lang-bash">docker run \
  --name demo-postgres \
  -p 5432:5432 \
  -e POSTGRES_PASSWORD=foopsw \
  postgres:16 \
  postgres -c <span class="hljs-string">"wal_level=logical"</span>
</code></pre>
<p>This command will start a Postgres instance with logical replication enabled, and you can connect to it using the following command:</p>
<pre><code class="lang-bash">docker <span class="hljs-built_in">exec</span> -it demo-postgres psql -U postgres
</code></pre>
<p>Now, from the Postgres shell, we can create a database and a table to work with:</p>
<pre><code class="lang-sql"><span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">TABLE</span> <span class="hljs-keyword">IF</span> <span class="hljs-keyword">NOT</span> <span class="hljs-keyword">EXISTS</span> foo (
  <span class="hljs-keyword">id</span> <span class="hljs-built_in">SERIAL</span> PRIMARY <span class="hljs-keyword">KEY</span>,
  col1 <span class="hljs-built_in">VARCHAR</span>,
  col2 <span class="hljs-built_in">INT</span>
);
</code></pre>
<p>To check if the table was created successfully, the following command lists the user's tables in the database:</p>
<pre><code class="lang-sql">\dt
</code></pre>
<h3 id="heading-creating-a-data-producer"><strong>Creating a data producer</strong></h3>
<p>Now that we have our Postgres instance set up, let's create a Node.js application that acts as a data producer. This application will insert data into the <code>foo</code> table we created earlier and represent the source of data replication. It could be a web application, an IoT device, or any other data source you may have.</p>
<p>Let's create a <code>producer.js</code> file and implement the following logic:</p>
<ul>
<li><p>It connects to the Postgres database.</p>
</li>
<li><p>Every second, it inserts a new row into the <code>foo</code> table or updates the last inserted row alternatively.</p>
</li>
<li><p>It listens for a <code>SIGINT</code> signal to gracefully close the connection.</p>
</li>
</ul>
<pre><code class="lang-js"><span class="hljs-keyword">import</span> pg <span class="hljs-keyword">from</span> <span class="hljs-string">'pg'</span> <span class="hljs-comment">// ℹ️ npm install pg</span>
<span class="hljs-keyword">const</span> { Client } = pg

<span class="hljs-keyword">const</span> client = <span class="hljs-keyword">new</span> Client({
  <span class="hljs-attr">user</span>: <span class="hljs-string">'postgres'</span>,
  <span class="hljs-attr">password</span>: <span class="hljs-string">'foopsw'</span>
})

<span class="hljs-keyword">await</span> client.connect()

<span class="hljs-keyword">let</span> counter = <span class="hljs-number">0</span>
<span class="hljs-keyword">let</span> lastId = <span class="hljs-number">0</span>
<span class="hljs-keyword">const</span> fakeUserInteraction = <span class="hljs-built_in">setInterval</span>(<span class="hljs-keyword">async</span> () =&gt; {
  <span class="hljs-keyword">if</span> (counter % <span class="hljs-number">2</span> === <span class="hljs-number">0</span>) {
    <span class="hljs-keyword">const</span> res = <span class="hljs-keyword">await</span> client.query(<span class="hljs-string">'INSERT INTO foo (col1, col2) VALUES ($1, $2) RETURNING id'</span>, [<span class="hljs-string">'Hello world!'</span>, counter])
    lastId = res.rows[<span class="hljs-number">0</span>].id
    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">`Inserted new row: <span class="hljs-subst">${lastId}</span>`</span>)
  } <span class="hljs-keyword">else</span> {
    <span class="hljs-keyword">await</span> client.query(<span class="hljs-string">'UPDATE foo SET col1 = $1, col2 = $2 WHERE id = $3'</span>, [<span class="hljs-string">'Hello world!!!'</span>, <span class="hljs-number">42</span>, lastId])
    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">`Updated row <span class="hljs-subst">${lastId}</span>`</span>)
  }

  counter++
}, <span class="hljs-number">1000</span>)

process.on(<span class="hljs-string">'SIGINT'</span>, <span class="hljs-keyword">async</span> () =&gt; {
  <span class="hljs-built_in">clearInterval</span>(fakeUserInteraction)
  <span class="hljs-keyword">await</span> client.end()
})
</code></pre>
<p>To run this code the <code>node producer.js</code> command is enough.</p>
<h3 id="heading-creating-a-data-consumer"><strong>Creating a data consumer</strong></h3>
<p>The data consumer is the other side of the replication process. It can be another Postgres instance, or even a different a different database engine. Postgres allows us to replicate the data seamlessly across different versions of the Postgres database just by configuring them accordingly (not the topic of this blog post, but there are good <a target="_blank" href="https://www.youtube.com/watch?v=3Z4Hhh5EnLA">video tutorials</a> for this use case).</p>
<p>Our use case is to replicate the data to a Node.js application! This application will connect to the Postgres database and listen for changes in the <code>foo</code> table using the logical replication mechanism.</p>
<h4 id="heading-setting-up-the-data-consumer"><strong>Setting up the data consumer</strong></h4>
<p>To set up the data consumer, we must introduce some new Postgres concepts:</p>
<ul>
<li><p><a target="_blank" href="https://www.postgresql.org/docs/16/logical-replication-publication.html"><strong>Publication</strong>:</a> A publication defines a set of tables and changes to be replicated. We must create a publication to specify which tables and changes are replicated.</p>
</li>
<li><p><a target="_blank" href="https://www.postgresql.org/docs/16/logical-replication-subscription.html"><strong>Subscription</strong></a>: A subscription defines the source of replication (e.g., primary server) and the target replica(s). We must create a subscription to subscribe to changes happening in the <code>foo</code> table.</p>
</li>
<li><p><a target="_blank" href="https://www.postgresql.org/docs/16/warm-standby.html#STREAMING-REPLICATION-SLOTS"><strong>Replication slot</strong></a>: A replication slot is like a bookmark in the WAL file that keeps track of the last WAL segment read by a client. It forces the primary server to retain the WAL segments required by any client that is using the replication slot.</p>
</li>
</ul>
<p>We need to create all these objects in the Postgres database before we can start listening for changes. Let's do it programmatically with Node.js by creating a <code>setup-consumer.js</code> file:</p>
<pre><code class="lang-js"><span class="hljs-keyword">import</span> pg <span class="hljs-keyword">from</span> <span class="hljs-string">'pg'</span>
<span class="hljs-keyword">const</span> { Client } = pg

<span class="hljs-keyword">const</span> client = <span class="hljs-keyword">new</span> Client({ <span class="hljs-attr">user</span>: <span class="hljs-string">'postgres'</span>, <span class="hljs-attr">password</span>: <span class="hljs-string">'foopsw'</span> })
<span class="hljs-keyword">await</span> client.connect()

<span class="hljs-keyword">await</span> createPublicationIfNotExists(<span class="hljs-string">'foo_even'</span>, <span class="hljs-string">'TABLE foo WHERE (id % 2 = 0);'</span>)
<span class="hljs-keyword">await</span> createPublicationIfNotExists(<span class="hljs-string">'foo_update_only'</span>, <span class="hljs-string">"TABLE foo WITH (publish = 'update');"</span>)

<span class="hljs-keyword">await</span> client.end()

<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">createPublicationIfNotExists</span> (<span class="hljs-params">pubName, condition</span>) </span>{
  <span class="hljs-keyword">const</span> pub = <span class="hljs-keyword">await</span> client.query(<span class="hljs-string">'SELECT * FROM pg_publication WHERE pubname = $1'</span>, [pubName])

  <span class="hljs-keyword">if</span> (!pub.rows.length) {
    <span class="hljs-keyword">await</span> client.query(<span class="hljs-string">`CREATE PUBLICATION <span class="hljs-subst">${pubName}</span> FOR <span class="hljs-subst">${condition}</span>`</span>)
    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">`Created publication <span class="hljs-subst">${pubName}</span>`</span>)
  } <span class="hljs-keyword">else</span> {
    <span class="hljs-keyword">await</span> client.query(<span class="hljs-string">`ALTER PUBLICATION <span class="hljs-subst">${pubName}</span> SET <span class="hljs-subst">${condition}</span>`</span>)
    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">`Updated publication <span class="hljs-subst">${pubName}</span>`</span>)
  }
}
</code></pre>
<p>The code shows how to create and update a publication. When a publication is created, it specifies:</p>
<p>1: Which tables and columns are included in the replication process?</p>
<ul>
<li><p>If no columns are specified, all columns are included.</p>
</li>
<li><p>It is possible to list multiple tables or set <code>FOR ALL TABLES;</code> to include all tables in the replication process.</p>
</li>
</ul>
<p>2: Which operations are included in the replication process?</p>
<ul>
<li><p>The default is to include all operations (inserts, updates, deletes).</p>
</li>
<li><p>It is possible to specify only inserts, only updates, or only deletes.</p>
</li>
</ul>
<p>We have created two publications in the code snippet above:</p>
<ul>
<li><p><code>foo_even</code>: it includes only rows where the <code>id</code> is an even number.</p>
</li>
<li><p><code>fooupdateonly</code>: it includes only updates.</p>
</li>
</ul>
<p>As shown, the publications are very flexible and can be tailored to specific use cases.</p>
<h4 id="heading-subscribing-to-changes"><strong>Subscribing to changes</strong></h4>
<p>Finally, we are ready to create the <code>consumer.js</code> file to listen for changes in the <code>foo</code> table. To do so, we will use the <code>postgres</code> module because it provides a simple way to consume the logical replication data changes or we should implement the <a target="_blank" href="https://www.postgresql.org/docs/16/protocol-logical-replication.html">logical replication protocol</a> ourselves. Alternatively, you can use the <code>pg-logical-replication</code> library — it depends on your preference.</p>
<p>To create the <code>consumer.js</code> file, we can use the following code snippet. So first we must connect to the Postgres database and list all the publications we want to subscribe to:</p>
<pre><code class="lang-js"><span class="hljs-keyword">import</span> postgres <span class="hljs-keyword">from</span> <span class="hljs-string">'postgres'</span>

<span class="hljs-keyword">const</span> sql = postgres({
  <span class="hljs-attr">debug</span>: <span class="hljs-literal">true</span>,
  <span class="hljs-attr">user</span>: <span class="hljs-string">'postgres'</span>,
  <span class="hljs-attr">password</span>: <span class="hljs-string">'foopsw'</span>,
  <span class="hljs-attr">publications</span>: [
    <span class="hljs-string">'foo_even'</span>,
    <span class="hljs-string">'foo_update_only'</span>
  ]
})
</code></pre>
<p>Then we can subscribe to the changes happening in the <code>foo</code> table:</p>
<pre><code class="lang-js"><span class="hljs-keyword">const</span> eventPattern = <span class="hljs-string">'*:foo'</span>
<span class="hljs-keyword">const</span> { unsubscribe } = <span class="hljs-keyword">await</span> sql.subscribe(
  eventPattern,
  <span class="hljs-function">(<span class="hljs-params">row, { command, relation, key, old }</span>) =&gt;</span> {
    <span class="hljs-built_in">console</span>.log({
      command,
      row
    })
  },

  <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">onConnect</span> (<span class="hljs-params"></span>) </span>{
    <span class="hljs-comment">// Callback on initial connect and potential reconnects</span>
    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Connected to the publication'</span>)
  }
)

process.on(<span class="hljs-string">'SIGINT'</span>, <span class="hljs-keyword">async</span> () =&gt; {
  <span class="hljs-keyword">await</span> unsubscribe()
  process.exit()
})
</code></pre>
<p>If we run the <code>node consumer.js</code> command, we will see the <code>console.log</code> output every time the <code>producer.js</code>:</p>
<ul>
<li><p>Insert a row with an even ID in the <code>foo</code> table.</p>
</li>
<li><p>Update a row in the <code>foo</code> table.</p>
</li>
</ul>
<p>Note that the <code>eventPattern</code> is just a feature of the <code>postgres</code> module that allows us to filter the changes we are getting from the database. The main driver is and will always be the Postgres publication we created earlier with the <code>setup-consumer.js</code> script.</p>
<p>So far we did not create the Replication Slot, but the <code>postgres</code> module will do it for us by calling the <code>CREATEREPLICATIONSLOT</code> command creating a <code>TEMPORARY</code> slot. Every subscription needs a Replication Slot to track the changes that they are consuming.</p>
<p>Note that the Replication Slot MUST be deleted or consumed by the client, otherwise — if it isn't then the WAL file in the primary server will not be truncated and the disk space will grow indefinitely! Luckily, the <code>postgres</code> module creates a temporary replication slot that is automatically deleted when the connection is closed.</p>
<p>Now our Node.js application is ready to listen for changes in the <code>foo</code> table and replicate them in real-time!</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>PostgreSQL's replication capabilities are a cornerstone of its reliability and scalability. We just scratched the surface of what is possible with Postgres replication management, but we have seen how to set up a real-time data replication system using Node.js and Postgres logical replication.</p>
<p>Another interesting use case is to use the logical replication mechanism to read all the changes happening in the database while the Node.js application was offline. This is possible by handling the Replication Slot manually and it can be done with the <code>pg-logical-replication</code> module's API, but this will be the topic of another blog post!</p>
<p>We hope this blog post has given you a good starting point to explore Postgres replication management further. Here are some resources to deepen your knowledge:</p>
<ul>
<li><p><a target="_blank" href="https://www.youtube.com/watch?v=WXKxwl9k5Q8">https://www.youtube.com/watch?v=WXKxwl9k5Q8</a></p>
</li>
<li><p><a target="_blank" href="https://www.youtube.com/watch?v=3Z4Hhh5EnLA">https://www.youtube.com/watch?v=3Z4Hhh5EnLA</a></p>
</li>
<li><p><a target="_blank" href="https://www.youtube.com/watch?v=E6W-ZWVRAHU">https://www.youtube.com/watch?v=E6W-ZWVRAHU</a></p>
</li>
</ul>
<p>Comment and share if you enjoyed this article!</p>
]]></content:encoded></item><item><title><![CDATA[Node.js release schedule explained]]></title><description><![CDATA[By Marco Ippolito Understanding the Node.js release process might seem tricky at first, especially if you're new or not familiar with all the details…]]></description><link>https://nearform.hashnode.dev/nodejs-release-schedule-explained</link><guid isPermaLink="true">https://nearform.hashnode.dev/nodejs-release-schedule-explained</guid><category><![CDATA[General Programming]]></category><category><![CDATA[Programming Blogs]]></category><category><![CDATA[iwritecode]]></category><category><![CDATA[Node.js]]></category><category><![CDATA[nodeJS22]]></category><dc:creator><![CDATA[Nearform]]></dc:creator><pubDate>Tue, 14 May 2024 14:10:57 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1715695415285/ecab0190-0605-42bb-aa06-3b02dd9b55ea.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>By Marco Ippolito</p>
<h2 id="heading-from-version-numbers-to-the-release-lifecycle-we-help-newcomers-get-familiar-with-the-nodejs-release-process">From version numbers to the release lifecycle, we help newcomers get familiar with the Node.js release process</h2>
<p><strong>Understanding the Node.js release process might seem tricky at first, especially if you're new or not familiar with all the details. However, mastering it not only makes developers’ lives easier but brings real value to businesses.</strong></p>
<p>Staying up to date not only reduces downtime and boosts software reliability but also minimises the risk of costly, large-scale migrations to newer versions. By constantly updating versions, developers save time that can be better spent on revenue-generating activities. This proactive approach translates into fewer security risks, faster performance and lower cloud costs. Altogether, it keeps businesses competitive, delights users and secures long-term success.</p>
<p>As a maintainer of Node.js, I've recently started serving as a releaser. This role has given me a good understanding of the release schedule, that's why I'm here to shed some light on how it all works.</p>
<h2 id="heading-odd-and-even-version-numbers">Odd and even version numbers</h2>
<p>It's essential to know that odd-numbered versions serve as a playground for developers.</p>
<p>These versions are active and primarily used by package authors and users to preview upcoming features. Odd-numbered major version releases (v17, v19, v21) will not be promoted to Long-Term Support (LTS).</p>
<p>However, even-numbered versions become LTS releases (v18, v20, v22).</p>
<p>Interestingly, each major LTS version of Node.js is named after an element from the periodic table (v18 Hydrogen, v20 Iron), adding a touch of chemistry to the release process.</p>
<h2 id="heading-understanding-the-nodejs-release-lifecycle">Understanding the Node.js release lifecycle</h2>
<p>Node.js follows a structured release schedule to ensure stability, compatibility, and longevity of support for its users. Here's a breakdown of how it all unfolds:</p>
<h3 id="heading-1-current-release-status">1. Current release status</h3>
<p>When a major version of Node.js is released, regardless of whether it is even or odd, it enters the <strong>Current</strong> release status. This phase lasts for six months. During this time, developers and library authors are encouraged to adopt and adapt to the new features and changes introduced in the latest release. It's a period of innovation and experimentation.</p>
<h3 id="heading-2-active-lts-status">2. Active LTS status</h3>
<p>After the initial six-month period, odd-numbered releases transition to unsupported status. Even-numbered releases, however, move into <strong>Active LTS</strong> status. These LTS releases are ready for general use and offer extended support.</p>
<p>This phase typically lasts for one year before entering the <strong>Maintenance LTS status.</strong></p>
<p>It’s worth mentioning that odd-version releases will receive security patches before the next even-numbered major enters the <strong>Active</strong> <strong>LTS</strong> status.</p>
<h3 id="heading-3-maintenance-lts-status">3. Maintenance LTS status</h3>
<p>While in Active LTS status, a Node.js version receives active support, including new features, fixes for critical bugs and security patches. After this period, the version enters Maintenance LTS status which lasts around 18 months, where only fixes for bugs and security patches are released.</p>
<h3 id="heading-4-end-of-life-eol">4. End of Life (EOL)</h3>
<p>Once a Node.js version reaches the end of its Maintenance LTS period, it is considered End of Life (EOL). At this point, it will no longer receive official support or updates from the Node.js project.</p>
<p>Node.js maintains three release lines simultaneously, two of which are LTS versions, one in Maintanance LTS, one in Active LTS, and the third one as Current (either a odd-numbered version or a even-numbered that has not entered Active status yet), ensuring a balance between innovation and stability.</p>
<p>Here is an image of the current status of Node.js releases from the <a target="_blank" href="https://nodejs.org/en/about/previous-releases">documentation</a>:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1715695558160/174af04b-ecf0-4d42-9739-3443d3ca44fe.png" alt class="image--center mx-auto" /></p>
<p>Recently, I had the pleasure of contributing to the release of <a target="_blank" href="https://www.nearform.com/digital-community/node-js-22-the-future-of-server-side-javascript/">Node.js 22</a>, which has currently entered the <strong>Current</strong> status, while the v21 version is being dismissed.</p>
<p>When Node v22 enters the Active LTS status, Node v20 enters the Maintanance Mode, and Node v23 is set as Current.</p>
<h3 id="heading-recommendations-for-production-applications">Recommendations for production applications</h3>
<p>For production applications, it's crucial to use Node.js versions that are either in Active LTS or Maintenance LTS status. These versions provide a stable and supported environment for your applications, ensuring reliability and that security fixes.</p>
<p>But don't overlook the importance of testing against Current versions, both odd and even, it's an essential step in keeping your production environment robust and up-to-date.</p>
<p>Odd versions of Node.js not only let you experiment with new features before the release of the next LTS version but also provide warnings for upcoming end-of-life deprecations expected in the next LTS release. This gives you ample time to plan and migrate your applications accordingly. On the other hand, even major versions help you ensure that your application won't break and give you time to prepare for changes.</p>
<p>By doing so, you can provide valuable feedback to the Node.js project and identify any potential compatibility issues early on.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>In conclusion, navigating the Node.js release process requires a nuanced understanding of its versioning system and lifecycle. From the distinction between odd and even version numbers to the structured phases of Current, Active LTS, Maintenance LTS, and End of Life, each step serves a vital role in balancing innovation with stability.</p>
<p>As Node.js continues to evolve, understanding its release lifecycle empowers developers to make informed decisions, maintain reliable applications, and contribute to the growth of the ecosystem. Embracing this process not only ensures the longevity of your projects but also fosters collaboration and innovation within the Node.js community.</p>
]]></content:encoded></item><item><title><![CDATA[How Node.js is helping you survive the IPv6 era]]></title><description><![CDATA[By Paolo Insogna Every day, pretty much everybody takes for granted the incredible amount of work and technology that makes the internet function. Even…]]></description><link>https://nearform.hashnode.dev/how-nodejs-is-helping-you-survive-the-ipv6-era</link><guid isPermaLink="true">https://nearform.hashnode.dev/how-nodejs-is-helping-you-survive-the-ipv6-era</guid><category><![CDATA[General Programming]]></category><category><![CDATA[iwritecode]]></category><category><![CDATA[Programming Blogs]]></category><category><![CDATA[Node.js]]></category><category><![CDATA[ipv6]]></category><dc:creator><![CDATA[Nearform]]></dc:creator><pubDate>Fri, 10 May 2024 10:24:48 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1715336249196/7b9f7710-9696-4bd3-a2b3-9b01cc0079b8.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>By Paolo Insogna</p>
<h2 id="heading-we-take-a-look-at-happy-eyeballs-a-crucial-tool-in-ensuring-seamless-connectivity-for-users">We take a look at Happy Eyeballs, a crucial tool in ensuring seamless connectivity for users</h2>
<p><strong>Every day, pretty much everybody takes for granted the incredible amount of work and technology that makes the internet function. Even developers and other technically-minded individuals often overlook or disregard the intricate details and tasks involved in establishing a connection to a remote host. Let's take a look at one of the details, which happens to have a rather fancy name: Happy Eyeballs.</strong></p>
<h2 id="heading-first-of-all-what-is-your-address">First of all, what is your address?</h2>
<p>When an application seeks to establish a connection to a remote host, it typically only has the hostname. A simple verification of this is that you probably have never typed <code>216.58.205.46</code> into your browser's address bar.</p>
<p>Upon receiving the connection request, the browser performs a <a target="_blank" href="https://it.wikipedia.org/wiki/Domain_Name_System">Domain Name System</a> (DNS) query to resolve the IP address corresponding to the hostname. This information is crucial because certain server application optimisations, such as load balancing, rely on it. Each DNS query can receive its own response, enabling the server to efficiently route clients based on factors like load and geographical position.</p>
<p>Once the client receives an IP address, it has all the necessary components to create a socket: source address, source port, destination address, and destination port.</p>
<h2 id="heading-ipv6-is-the-future">IPv6 is the future</h2>
<p>One detail often overlooked is the IP protocol version. The most well-known and widely supported version is IPv4, where addresses are comprised of four numbers ranging from 0 to 255.</p>
<p>IPv4 has been in use since the 1980s and is still the most commonly used version. However, we're running out of IPv4 addresses. IPv4 theoretically supports only 4 billion addresses, but in practice, due to various technical reasons, the number is much lower.</p>
<p>To address this issue, IPv6 was introduced in 1998 as the successor to IPv4. IPv6 offers a staggering number of addresses ($3.8 \cdot 10^{38}$, to be precise) ensuring we'll never run out. Despite gaining traction in recent years, IPv4 remains the preferred choice, especially for Internet Service Providers and router manufacturers.</p>
<p>This preference can pose a problem; if a destination host only exposes an IPv6 address via DNS and your router or ISP isn't properly configured for IPv6, you may be unable to connect. Similarly, if both IPv4 and IPv6 addresses are provided via DNS but the IPv6 address is listed first, some frameworks may attempt only the first address, resulting in connection failures. This <a target="_blank" href="https://github.com/nodejs/node/issues/41625">was the case</a> for Node.js before version 19.3.0.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1715336468292/f1f13fef-be95-447a-b2b3-289a1e8fd193.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-happy-eyeballs">Happy Eyeballs</h2>
<p>To address the challenges associated with misconfigured IPv6 connectivity stacks, the <a target="_blank" href="https://www.rfc-editor.org/rfc/rfc6555">Happy Eyeballs RFC</a> (Request for Comments) was introduced in 2012. The whimsical name stems from the term "eyeball," which refers to endpoints representing human internet end-users. The RFC aimed to ensure users wouldn't notice any difference when connecting to a host via IPv6 compared to IPv4.</p>
<p>In 2017, a <a target="_blank" href="https://www.rfc-editor.org/rfc/rfc8305.html">new version of the RFC</a> was released, proposing a clever solution to the connection problem. If a DNS query returns multiple A (IPv4) and AAAA (IPv6) records, the client must sort them in an alternate order: IPv4, then IPv6, then IPv4 again, and so forth. The code snippet below shows <a target="_blank" href="https://github.com/nodejs/node/blob/3790d524c1781334ccc504a6eb510bc53ee2ad64/lib/net.js#L1477">how the sorting is currently done</a> in Node.js.</p>
<pre><code class="lang-js"><span class="hljs-comment">/*
 validAddress is a tuple of two arrays. 
 Each of them contains either IPv4 or IPv6 only.
 The order of the addresses in the array is the same of DNS response.

 If the first address in DNS response contained a IPv4 address, then
 validAddresses[0] will contain IPv4 only, otherwise is the other way round.
*/</span>
<span class="hljs-keyword">const</span> toAttempt = [];
<span class="hljs-keyword">for</span> (<span class="hljs-keyword">let</span> i = <span class="hljs-number">0</span>, l = MathMax(validAddresses[<span class="hljs-number">0</span>].length, validAddresses[<span class="hljs-number">1</span>].length); i &lt; l; i++) {
  <span class="hljs-keyword">if</span> (i <span class="hljs-keyword">in</span> validAddresses[<span class="hljs-number">0</span>]) {
    ArrayPrototypePush(toAttempt, validAddresses[<span class="hljs-number">0</span>][i]);
  }
  <span class="hljs-keyword">if</span> (i <span class="hljs-keyword">in</span> validAddresses[<span class="hljs-number">1</span>]) {
    ArrayPrototypePush(toAttempt, validAddresses[<span class="hljs-number">1</span>][i]);
  }
}
</code></pre>
<p>The client then attempts a connection to the first IP and subsequently to the second IP after a short interval. Importantly, the client doesn't wait for a connection attempt to succeed; once any attempt succeeds, the connection is established, and all other attempts are abandoned.</p>
<p>This approach ensures that regardless of how the DNS records are returned, the client will always be able to choose the best-supported stack.</p>
<h2 id="heading-what-about-nodejs">What about Node.js?</h2>
<p>As mentioned earlier, Node.js lacked support for Happy Eyeballs until version 19.3.0, when the <code>autoSelectFamily</code> option was added to <code>net.connect</code> (and made the default in Node.js 20). Prior to this, Node.js would only attempt to connect to the first resolved IPs, regardless of the family, resulting in numerous connection failures.</p>
<p>Due to the internals of Node, Happy Eyeballs was implemented in a loose manner. Although there's never more than one connection attempt to a host, addresses are still attempted in alternate order and with a short, configurable timeout (defaulting to 250ms). This ensures adherence to the RFC's core idea, and subsequently, Node.js is able to correctly choose the right IP to establish a connection. The snippet below shows how Node.js <a target="_blank" href="https://github.com/nodejs/node/blob/3790d524c1781334ccc504a6eb510bc53ee2ad64/lib/net.js#L1701">handles the connection timeouts</a> during the Happy Eyeballs algorithm.</p>
<pre><code class="lang-js"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">internalConnectMultipleTimeout</span>(<span class="hljs-params">context, req, handle</span>) </span>{
  debug(<span class="hljs-string">'connect/multiple: connection to %s:%s timed out'</span>, req.address, req.port);
  context.socket.emit(<span class="hljs-string">'connectionAttemptTimeout'</span>, req.address, req.port, req.addressType);

  req.oncomplete = <span class="hljs-literal">undefined</span>;
  ArrayPrototypePush(context.errors, createConnectionError(req, UV_ETIMEDOUT));
  handle.close();

  <span class="hljs-comment">// Try the next address, unless we were aborted</span>
  <span class="hljs-keyword">if</span> (context.socket.connecting) {
    internalConnectMultiple(context);
  }
}
</code></pre>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Navigating a dual-stack world where IPv4 and IPv6 coexist requires careful handling, especially in resolving host addresses and establishing connections.</p>
<p>Happy Eyeballs, with its intelligent approach to address resolution and connection attempts, serves as a crucial tool in ensuring seamless connectivity for users, irrespective of the underlying IP protocol version.</p>
<p>With its adoption in frameworks like Node.js, the internet continues to evolve, maintaining its accessibility and reliability for all users.</p>
]]></content:encoded></item><item><title><![CDATA[Node.js 22: The future of server-side JavaScript]]></title><description><![CDATA[By Marco Ippolito With the release of version 22, Node.js continues to push boundaries and offer developers new tools and enhancements to create powerful…]]></description><link>https://nearform.hashnode.dev/nodejs-22-the-future-of-server-side-javascript</link><guid isPermaLink="true">https://nearform.hashnode.dev/nodejs-22-the-future-of-server-side-javascript</guid><category><![CDATA[General Programming]]></category><category><![CDATA[Programming Blogs]]></category><category><![CDATA[iwritecode]]></category><category><![CDATA[Node.js]]></category><category><![CDATA[nodeJS22]]></category><dc:creator><![CDATA[Nearform]]></dc:creator><pubDate>Mon, 22 Apr 2024 23:00:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1715335387848/048cbba9-273d-43a6-8583-7480306de0f2.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>By Marco Ippolito</p>
<h2 id="heading-discover-nodejs-22s-exciting-new-features-and-improvements">Discover Node.js 22’s exciting new features and improvements</h2>
<p><strong>With the release of version 22, Node.js continues to push boundaries and offer developers new tools and enhancements to create powerful and efficient applications. In this blog post, we'll explore some of the exciting features and improvements introduced in Node.js 22.</strong></p>
<h2 id="heading-v8-gets-updated-to-version-124">V8 gets updated to version 12.4</h2>
<p>This Node.js version includes an important update of V8 to version 12.4, packed with new features and improvements. Here's what's included in this update:</p>
<h4 id="heading-webassembly-garbage-collection">WebAssembly Garbage Collection</h4>
<p>One of the highlights of this release is the introduction of <a target="_blank" href="https://github.com/WebAssembly/gc/blob/main/proposals/gc/Overview.md">WebAssembly Garbage Collection</a>. This feature enhances memory management for WebAssembly code, leading to improved performance and efficiency in your applications.</p>
<h4 id="heading-arrayfromasync">Array.fromAsync</h4>
<p>Introducing <code>Array.fromAsync</code>, a new method that allows you to asynchronously create arrays from async iterables. Here's a quick example of how you can use it:</p>
<pre><code class="lang-js"><span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span>* <span class="hljs-title">asyncIterable</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">for</span> (<span class="hljs-keyword">let</span> i = <span class="hljs-number">0</span>; i &lt; <span class="hljs-number">5</span>; i++) {
    <span class="hljs-keyword">await</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Promise</span>(<span class="hljs-function">(<span class="hljs-params">resolve</span>) =&gt;</span> <span class="hljs-built_in">setTimeout</span>(resolve, <span class="hljs-number">10</span> * i));
    <span class="hljs-keyword">yield</span> i;
  }
}

<span class="hljs-keyword">const</span> array = <span class="hljs-built_in">Array</span>.fromAsync(asyncIterable);
<span class="hljs-comment">// [0, 1, 2, 3, 4]</span>
</code></pre>
<h4 id="heading-sets-method">Sets method</h4>
<p>Several new methods are added to Sets, including union, intersection, and difference. These methods make it easier to manipulate Sets and perform common operations. Check out the following examples:</p>
<pre><code class="lang-js"><span class="hljs-keyword">const</span> lts = <span class="hljs-keyword">new</span> <span class="hljs-built_in">Set</span>([<span class="hljs-number">18</span>, <span class="hljs-number">20</span>, <span class="hljs-number">22</span>]);
<span class="hljs-keyword">const</span> nonLts = <span class="hljs-keyword">new</span> <span class="hljs-built_in">Set</span>([<span class="hljs-number">17</span>, <span class="hljs-number">19</span>, <span class="hljs-number">21</span>]);

<span class="hljs-keyword">const</span> all = lts.union(nonLts); <span class="hljs-comment">// all versions [18, 20, 22, 17, 19, 21]</span>

all.intersection(nonLts); <span class="hljs-comment">// only values contained in both sets [17, 19, 21]</span>

all.difference(nonLts); <span class="hljs-comment">// only values not contained in both sets [18, 20, 22]</span>
</code></pre>
<h4 id="heading-iterator-helpers">Iterator helpers</h4>
<p>These helpers include methods like take, drop, and map, making it easier to manipulate and process iterator values. Here are some examples:</p>
<pre><code class="lang-js"><span class="hljs-function"><span class="hljs-keyword">function</span>* <span class="hljs-title">fibonacci</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">let</span> current = <span class="hljs-number">1</span>;
  <span class="hljs-keyword">let</span> next = <span class="hljs-number">1</span>;
  <span class="hljs-keyword">while</span> (<span class="hljs-literal">true</span>) {
    <span class="hljs-keyword">yield</span> current;
    [current, next] = [next, current + next];
  }
}

<span class="hljs-comment">// Prints only the first 5 values</span>
<span class="hljs-keyword">for</span> (<span class="hljs-keyword">const</span> n <span class="hljs-keyword">of</span> fibonacci().take(<span class="hljs-number">5</span>)) {
   <span class="hljs-built_in">console</span>.log(n);
}

<span class="hljs-comment">// Takes the first five elements, then drops the first two</span>
<span class="hljs-keyword">for</span> (<span class="hljs-keyword">const</span> n <span class="hljs-keyword">of</span> fibonacci().take(<span class="hljs-number">5</span>).drop(<span class="hljs-number">2</span>)) {
  <span class="hljs-built_in">console</span>.log(n);
}

<span class="hljs-comment">// Elevates every number at power of 2</span>
<span class="hljs-keyword">for</span> (<span class="hljs-keyword">const</span> n <span class="hljs-keyword">of</span> fibonacci().map(<span class="hljs-function">(<span class="hljs-params">x</span>) =&gt;</span> x ** <span class="hljs-number">2</span>)) {
  <span class="hljs-built_in">console</span>.log(n);
}
</code></pre>
<h2 id="heading-requireing-esm-modules">require()ing ESM modules</h2>
<p>One of the most anticipated features in Node.js 22 is the ability to require ECMAScript modules (ESM) synchronously. With the <code>--experimental-require-module</code> flag, developers can now load ESM modules using <code>require()</code>. This feature addresses a common pain point for developers transitioning from CommonJS to ESM. Package authors can now gradually migrate to ESM without disrupting the user experience or bloating the <code>node_modules</code> directory with duplicated modules.</p>
<p>Exporting a ESM module:</p>
<pre><code class="lang-js"><span class="hljs-comment">// sum.mjs</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">sum</span>(<span class="hljs-params">a, b</span>) </span>{ <span class="hljs-keyword">return</span> a + b }
</code></pre>
<p>A ESM module can now be used in a CommonJS module:</p>
<pre><code class="lang-js"><span class="hljs-comment">// index.cjs</span>
<span class="hljs-keyword">const</span> sum = <span class="hljs-built_in">require</span>(<span class="hljs-string">'./sum.mjs'</span>)
<span class="hljs-built_in">console</span>.log(sum(<span class="hljs-number">1</span>,<span class="hljs-number">2</span>)) <span class="hljs-comment">// it works!</span>
</code></pre>
<pre><code class="lang-bash">node --experimental-require-module index.cjs
</code></pre>
<p>It's worth noting that while synchronous ESM graphs offer significant benefits, there are still some <a target="_blank" href="https://joyeecheung.github.io/blog/2024/03/18/require-esm-in-node-js/">edge cases and feature interactions</a> to consider. For example, interactions with other experimental features or scenarios involving cyclic dependencies may require further refinement. However, as an initial iteration of an experimental feature, Node.js 22 lays the groundwork for future improvements and iterations.</p>
<h2 id="heading-websocket-client">WebSocket client</h2>
<p>In Node.js 22 the WebSocket client implementation, exported from Undici, is now enabled by default, eliminating the need for external dependencies. This built-in support for WebSockets simplifies the development process and ensures compatibility with modern web standards.</p>
<pre><code class="lang-js"><span class="hljs-comment">// Create WebSocket connection</span>
<span class="hljs-keyword">const</span> socket = <span class="hljs-keyword">new</span> WebSocket(<span class="hljs-string">"ws://localhost:8080"</span>);

<span class="hljs-comment">// Connection opened</span>
socket.addEventListener(<span class="hljs-string">"open"</span>, <span class="hljs-function">(<span class="hljs-params">event</span>) =&gt;</span> {
  socket.send(<span class="hljs-string">"Hello Server!"</span>);
});
</code></pre>
<h2 id="heading-packagejson-script-execution">Package.json script execution</h2>
<p>With the new <code>--run</code> flag, developers can now execute scripts directly from the <code>package.json</code>. This experimental feature simplifies the execution of common tasks and workflows defined in the project's <code>package.json</code>, improving the development experience.</p>
<p>The <code>--run</code> flag allows running a specified command from a <code>package.json</code>'s <code>"scripts"</code> property. If no specific <code>"command"</code> is provided, it will list all available scripts.</p>
<p>When using <code>--run</code>, <code>node</code> prepends <code>./node_modules/.bin</code>, relative to the current working directory, to the <code>PATH</code>. This enables the execution of binaries from dependencies.</p>
<p>To execute the <code>test</code> script from <code>package.json</code> in the current folder, simply run:</p>
<pre><code class="lang-js">node --run test
</code></pre>
<p>You can also pass arguments to the command. Any argument after <strong>--</strong> will be appended to the script:</p>
<pre><code class="lang-text">node --run test -- --verbose
</code></pre>
<p>It's important to note that <code>node --run</code> is not designed to replicate the behaviours of <code>npm run</code> or similar commands from other package managers. Instead, it prioritises top performance for common use cases by intentionally limiting certain functionalities:</p>
<ul>
<li><p>It doesn't search for <code>package.json</code> files outside the current folder.</p>
</li>
<li><p>The <code>.bin</code> or <code>node_modules/.bin</code> paths of folders outside the current folder are not prepended.</p>
</li>
<li><p><code>pre</code> or <code>post</code> scripts are not executed alongside the specified script.</p>
</li>
<li><p>Package manager-specific environment variables cannot be defined.</p>
</li>
</ul>
<h2 id="heading-stable-watch-mode">Stable Watch Mode</h2>
<p>Watch Mode, previously an experimental feature, is now considered stable.</p>
<p>For those unfamiliar, Watch Mode is a feature that automatically restarts the Node.js process whenever changes are detected in watched files. This means no more manual restarts or interruptions during development and testing, simply add the <code>--watch</code> flag when starting your Node.js application.</p>
<p>By default, it monitors the entry point file and any required or imported modules. However, you can customise this behaviour using the <code>--watch-path</code> flag to specify specific paths to watch.</p>
<p>For example:</p>
<pre><code class="lang-text">node --watch index.js
</code></pre>
<p>And if you want to specify custom paths:</p>
<pre><code class="lang-text">node --watch-path=./src --watch-path=./tests index.js
</code></pre>
<p>It's important to note that <code>--watch-path</code> is currently supported only on macOS and Windows. Attempting to use it on unsupported platforms will result in an exception.</p>
<h2 id="heading-glob-and-globsync">Glob and GlobSync</h2>
<p>This release introduces glob and globSync functions for pattern matching. Developers can now utilise these functions for matching file paths based on specified patterns.</p>
<pre><code class="lang-js"><span class="hljs-keyword">import</span> { glob } <span class="hljs-keyword">from</span> <span class="hljs-string">'node:fs'</span>;

glob(<span class="hljs-string">'**/*.js'</span>, <span class="hljs-function">(<span class="hljs-params">err, matches</span>) =&gt;</span> {
  <span class="hljs-keyword">if</span> (err) <span class="hljs-keyword">throw</span> err;
  <span class="hljs-built_in">console</span>.log(matches);
});
</code></pre>
<p>JavaScript Copy to clipboard</p>
<pre><code class="lang-js"><span class="hljs-keyword">import</span> { globSync } <span class="hljs-keyword">from</span> <span class="hljs-string">'node:fs'</span>;

<span class="hljs-built_in">console</span>.log(globSync(<span class="hljs-string">'**/*.js'</span>));
</code></pre>
<h2 id="heading-improved-performance-with-maglev">Improved performance with Maglev</h2>
<p>The introduction of V8's <a target="_blank" href="https://v8.dev/blog/maglev">Maglev Compiler</a>, enabled by default on supported architectures, brings significant performance improvements to short-lived CLI programs.</p>
<p>Maglev follows <a target="_blank" href="https://benediktmeurer.de/2017/03/01/v8-behind-the-scenes-february-edition">TurboFan</a> and <a target="_blank" href="https://v8.dev/blog/sparkplug">SparkPlug</a> as the third compiler in the V8 engine's lineup.</p>
<p>TurboFan produces highly optimised machine code, albeit with a longer compilation time compared to SparkPlug, which prioritises compilation speed over optimisation. However, Maglev bridges this gap by generating code that is not only significantly faster than Sparkplug's but also compiled much more swiftly than TurboFan's.</p>
<p>By optimising CPU usage, Maglev enhances the overall efficiency of Node.js applications, resulting in faster execution times and better resource utilisation.</p>
<h2 id="heading-stream-default-high-water-mark">Stream default High Water Mark</h2>
<p>In Node.js 22, the default High Water Mark for streams is increased from 16KiB to 64KiB, resulting in a noticeable performance boost. While this change might use a bit more memory, the improvements in speed and efficiency outweigh any downsides, especially for applications handling large amounts of data. This update reflects the increased capabilities of modern machines since Node.js creation, following years of progress and technological advancements.</p>
<h2 id="heading-deprecations-and-removals">Deprecations and removals</h2>
<p>As part of ongoing maintenance and improvement efforts, Node.js 22 also includes several deprecations and removals aimed at streamlining the codebase and promoting best practices. Notably, a lot of crypto constructors that were not supposed to be public and old util methods have been removed. Developers are encouraged to review the list of deprecations and plan accordingly to ensure compatibility with future releases of Node.js.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Node.js 22 represents another significant milestone in the evolution of the Node.js platform, introducing new features, performance enhancements, and improvements to the development experience.</p>
<p>We encourage you to explore the latest release of Node.js 22 and try these new features.</p>
<p>As always, feedback and contributions from the community play a crucial role in shaping the future of Node.js, so don't hesitate to get involved and share your thoughts and ideas.</p>
]]></content:encoded></item><item><title><![CDATA[An NPM package to simplify GraphQL relay-style pagination]]></title><description><![CDATA[By Luke Jones We introduce you to therelay-pagination-directive, a new package from Nearform. This package reduces the boilerplate required to implement…]]></description><link>https://nearform.hashnode.dev/an-npm-package-to-simplify-graphql-relay-style-pagination</link><guid isPermaLink="true">https://nearform.hashnode.dev/an-npm-package-to-simplify-graphql-relay-style-pagination</guid><category><![CDATA[General Programming]]></category><category><![CDATA[iwritecode]]></category><category><![CDATA[Programming Blogs]]></category><category><![CDATA[GraphQL]]></category><category><![CDATA[Pagination]]></category><dc:creator><![CDATA[Nearform]]></dc:creator><pubDate>Fri, 12 Apr 2024 10:24:40 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1712916326026/d74639ee-a5b9-4baa-b9d3-16ba4d087df8.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>By Luke Jones</p>
<h2 id="heading-add-pagination-to-your-graphql-schema-without-the-boilerplate">Add pagination to your GraphQL schema without the boilerplate</h2>
<p><strong>We introduce you to the</strong><a target="_blank" href="https://github.com/nearform/relay-pagination-directive#relay-pagination-directive"><strong>relay-pagination-directive</strong></a><strong>, a new package from Nearform. This package reduces the boilerplate required to implement cursor-based pagination in GraphQL.</strong></p>
<p>You can add the directive to your schema, tag the fields you wish to paginate and all of the types will be generated for you as well as wrapping your resolver responses to create the Connection model objects. All you need to do is update your data queries.</p>
<p>The benefit of this is it reduces work duplication. This means you have more time to work on revenue-generating tasks, such as designing, developing and delivering new products and features.</p>
<h2 id="heading-the-problem">The problem</h2>
<p>At some point, you’ll need to implement pagination within your GraphQL server. Whether to support a paginated UI function or to enable loading large payloads without crashing your server.</p>
<p>The GraphQL <a target="_blank" href="https://graphql.org/learn/pagination/">documentation</a> describes the ‘Connection Model’ as the best approach to solving pagination within GraphQL. To implement this we need to make the following changes:</p>
<ul>
<li><p>Create <code>Connection</code> and <code>Edge</code> types for each type we wish to paginate.</p>
</li>
<li><p>Add <code>first</code> and <code>after</code> args to the schema field (forward only pagination).</p>
</li>
<li><p>Update your data queries to limit the response size, based on the <code>first</code> value, and filter based on the <code>after</code> value. Ensure your query has a consistent sort order.</p>
</li>
<li><p>Update the resolvers of each type to return the <code>Connection</code> object. Within this map your dataset to match the <code>Edge</code> type.</p>
</li>
</ul>
<p>This can generate a lot of extra work and 90% of the time you’re replicating the effort for each type you wish to paginate.</p>
<h2 id="heading-making-things-easier">Making things easier</h2>
<p>If you've used GQL directives before you're ready to use this library. Here's an example of integrating the directive with Mercurius. The functionality of this directive happens within the GraphQL execution, so this should be similar for any provider.</p>
<pre><code class="lang-js"><span class="hljs-keyword">import</span> { connectionDirective } <span class="hljs-keyword">from</span> <span class="hljs-string">'relay-pagination-directive'</span>
<span class="hljs-keyword">import</span> Fastify <span class="hljs-keyword">from</span> <span class="hljs-string">'fastify'</span>
<span class="hljs-keyword">import</span> mercurius <span class="hljs-keyword">from</span> <span class="hljs-string">'mercurius'</span>

<span class="hljs-keyword">const</span> { connectionDirectiveTypeDefs, connectionDirectiveTransformer } =
  connectionDirective()

<span class="hljs-keyword">const</span> typeDefs = <span class="hljs-string">`
  type People {
    id: ID!
    name: String!
    type: String!
  }

  type Query {
    people: People @connection // Directive added to the type we wish to 
                              // paginate 
  }
`</span>

<span class="hljs-keyword">const</span> resolvers = {
  <span class="hljs-attr">Query</span>: {
    <span class="hljs-attr">people</span>: <span class="hljs-keyword">async</span> (root, { first, after }, { app }) =&gt; {
      <span class="hljs-keyword">return</span> app.db.getUsers(first, after)
    },
  },
}

<span class="hljs-comment">// Include the provided directive type definition along with your</span>
<span class="hljs-comment">// application types</span>
<span class="hljs-keyword">const</span> schema = makeExecutableSchema({
  <span class="hljs-attr">typeDefs</span>: [connectionDirectiveTypeDefs, typeDefs],
  resolvers
})

<span class="hljs-comment">// Apply the directive schema transformer </span>
<span class="hljs-keyword">const</span> connectionSchema = connectionDirectiveTransformer(schema)

app.register(mercurius, {
  <span class="hljs-attr">schema</span>: connectionSchema,
  <span class="hljs-attr">graphiql</span>: <span class="hljs-literal">true</span>
})

<span class="hljs-keyword">await</span> app.listen({ <span class="hljs-attr">port</span>: <span class="hljs-number">3000</span> })
</code></pre>
<p>Did you notice how we've decorated the <code>people</code> query with the <code>@connection</code> directive? By adding this and using the <code>connectionDirectiveTransformer</code> to wrap your schema you're ready to paginate your users list.</p>
<p>The <code>@connection</code> directive will take the GQL type it's been applied to and generate the related <code>Connection</code> and <code>Edge</code> for you. Following this example, our generated schema would now look like this.</p>
<pre><code class="lang-graphql"><span class="hljs-keyword">type</span> PageInfo {
  <span class="hljs-symbol">startCursor:</span> String
  <span class="hljs-symbol">endCursor:</span> String
  <span class="hljs-symbol">hasNextPage:</span> Boolean!
  <span class="hljs-symbol">hasPreviousPage:</span> Boolean!
}

<span class="hljs-keyword">type</span> People {
  <span class="hljs-symbol">id:</span> ID!
  <span class="hljs-symbol">name:</span> String!
  <span class="hljs-symbol">type:</span> String!
}

<span class="hljs-keyword">type</span> PeopleEdge {
  <span class="hljs-symbol">node:</span> People!
  <span class="hljs-symbol">cursor:</span> ID!
}

<span class="hljs-keyword">type</span> PeopleConnection {
  <span class="hljs-symbol">edges:</span> [PeopleEdge!]!
  <span class="hljs-symbol">pageInfo:</span> PageInfo
}

<span class="hljs-keyword">type</span> Query {
  people(<span class="hljs-symbol">first:</span> Int!, <span class="hljs-symbol">after:</span> ID): PeopleConnection
}
</code></pre>
<p>As well as generating these types for you, the <code>people</code> query resolver will be wrapped so that the existing result will be transformed to match the Connection Model schema. This means your existing resolver response can remain unchanged.</p>
<p>One change you will need to make is updating your database queries to respect the <code>first</code> and <code>after</code> values. You’ll also need to ensure your query sorting will be the same across all page queries. Here is an example of converting a simple ‘get all’ query to support pagination.</p>
<pre><code class="lang-sql"><span class="hljs-comment">/* Original query */</span>
<span class="hljs-keyword">select</span> <span class="hljs-keyword">id</span>, <span class="hljs-keyword">name</span>
<span class="hljs-keyword">from</span> <span class="hljs-keyword">users</span>

<span class="hljs-comment">/* Updated to support pagination */</span>
<span class="hljs-keyword">select</span> <span class="hljs-keyword">id</span>, <span class="hljs-keyword">name</span>
<span class="hljs-keyword">from</span> <span class="hljs-keyword">users</span>
<span class="hljs-keyword">where</span> <span class="hljs-keyword">id</span> &lt; <span class="hljs-number">10</span> <span class="hljs-comment">-- If 'after' is supplied add the where clause to get the next page</span>
<span class="hljs-keyword">order</span> <span class="hljs-keyword">by</span> <span class="hljs-keyword">id</span> <span class="hljs-keyword">desc</span> <span class="hljs-comment">-- Ensuring consistent ordering</span>
<span class="hljs-keyword">limit</span> <span class="hljs-number">10</span> <span class="hljs-comment">-- The 'first' value restricting the response size</span>
</code></pre>
<p>Full examples of implementing pagination support are available in the <a target="_blank" href="https://github.com/nearform/relay-pagination-directive/blob/master/examples/hasNextPageStrategies.js">repository</a></p>
<p>Continuing with the above example, our resolver returns an array of <code>People</code> records. An array response from the resolver is assumed to be a collection of nodes and so the transformed result will look like this:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1712916671173/86cd31aa-45f0-43fb-826d-e9bb3003a287.webp" alt class="image--center mx-auto" /></p>
<h2 id="heading-modelling-multi-edge-relationships">Modelling multi-edge relationships</h2>
<p>Often we need to represent multiple relationships to the same model.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1712916945506/fb4601e2-8edf-43d3-b4fa-da4a89fbf9de.webp" alt class="image--center mx-auto" /></p>
<p><em>Graph data entity relationship diagram</em></p>
<p>Within GraphQL, this is done by adding properties to the edge between two nodes. This means we need to create multiple Connections/Edges for the same base type. This is achieved in <code>relay-pagination-directive</code> by supplying a <code>prefix</code> argument when adding the directive to your types.</p>
<pre><code class="lang-graphql"><span class="hljs-keyword">type</span> Film {
    <span class="hljs-symbol">id:</span> ID!
    <span class="hljs-symbol">name:</span> String!
    <span class="hljs-symbol">release:</span> Int!
  }

  <span class="hljs-keyword">type</span> People {
    <span class="hljs-symbol">id:</span> ID!
    <span class="hljs-symbol">name:</span> String!
    <span class="hljs-symbol">type:</span> String!
    <span class="hljs-symbol">films:</span> Film! <span class="hljs-meta">@connection</span>(<span class="hljs-symbol">prefix:</span> <span class="hljs-string">"PeopleFilm"</span>)
  }

  <span class="hljs-keyword">type</span> Cinema {
    <span class="hljs-symbol">id:</span> ID!
    <span class="hljs-symbol">name:</span> String!
    <span class="hljs-symbol">films:</span> Film! <span class="hljs-meta">@connection</span>(<span class="hljs-symbol">prefix:</span> <span class="hljs-string">"CinemaFilm"</span>)
  }

  <span class="hljs-keyword">type</span> Query {
    <span class="hljs-symbol">people:</span> [People!]! <span class="hljs-meta">@connection</span>
    <span class="hljs-symbol">cinemas:</span> Cinema <span class="hljs-meta">@connection</span>
  }
</code></pre>
<p>In this instance, the following types will be generated:</p>
<ul>
<li><p><code>PeopleEdge</code> and <code>PeopleConnection</code> for <code>Query.people</code></p>
</li>
<li><p><code>PeopleFilmEdge</code> and <code>PeopleFilmConnection</code> for <code>People.films</code></p>
</li>
<li><p><code>CinemaEdge</code> and <code>CinemaConnection</code> for <code>Query.cinemas</code></p>
</li>
<li><p><code>CinemaFilmEdge</code> and <code>CinemaFilmConnection</code> for <code>Cinema.films</code></p>
</li>
</ul>
<p>As shown above, we have different fields available for the <code>PeopleFilm</code> and <code>CinemaFilm</code> relationships (<code>roles</code>, <code>performance</code>, <code>showTimes</code> etc). So that these properties are added to our generated types, you need to provide their definitions via the configuration object that is passed to the <code>connectionDirective</code> function.</p>
<pre><code class="lang-js"><span class="hljs-keyword">const</span> typeOptionsMap = {
  <span class="hljs-attr">People</span>: {
    <span class="hljs-attr">edgeProps</span>: {
      <span class="hljs-attr">PeopleFilm</span>: {
        <span class="hljs-attr">roles</span>: <span class="hljs-string">'[String!]!'</span>,
        <span class="hljs-attr">performance</span>: <span class="hljs-string">'Int!'</span>
      }
    }
  },
  <span class="hljs-attr">Cinema</span>: {
    <span class="hljs-attr">edgeProps</span>: {
      <span class="hljs-attr">CinemaFilm</span>: {
        <span class="hljs-attr">price</span>: <span class="hljs-string">'Int1'</span>,
        <span class="hljs-attr">showTimes</span>: <span class="hljs-string">'JSON'</span>
      }
    }
  }
}

<span class="hljs-keyword">const</span> { connectionDirectiveTypeDefs, connectionDirectiveTransformer } =
  connectionDirective(typeConnectionMap)
</code></pre>
<h2 id="heading-generating-opaque-cursors">Generating opaque cursors</h2>
<p>GraphQL pagination suggests the use of opaque string values for our cursors. That is a string which holds no referential value when viewed by a user, generally a base64 encoded string. This provides us with flexibility over the server pagination implementation as the value holds no meaning to the consumer.</p>
<p>By default, we will look for an <code>id</code> property on your nodes and use this, along with the name of the GraphQL type, to generate a cursor value. It's possible to use a different property for the cursor value or to provide an encoding function to generate a cursor. It is also possible to skip cursor generation altogether and just use a property value that exists on your objects.</p>
<p>You can decide on the cursor implementation strategy for your application on a per-type basis using the <code>typeOptionsMap</code> configuration object as shown below:</p>
<pre><code class="lang-js"><span class="hljs-keyword">import</span> { encodeCursor, connectionDirective } <span class="hljs-keyword">from</span> <span class="hljs-string">'relay-pagination-directive'</span>

<span class="hljs-keyword">const</span> typeOptionsMap = {
  <span class="hljs-attr">People</span>: {
    <span class="hljs-attr">encodeCursor</span>: <span class="hljs-literal">false</span>, <span class="hljs-comment">// Don't use any encoding, pick the cursor value from</span>
    <span class="hljs-attr">cursorPropOrFn</span>: <span class="hljs-string">'otherId'</span> <span class="hljs-comment">// the 'otherId' property</span>
  },
  <span class="hljs-attr">Cinema</span>: {
    <span class="hljs-attr">encodeCursor</span>: <span class="hljs-literal">true</span>, <span class="hljs-comment">// the default option</span>
    <span class="hljs-attr">cursorPropOrFn</span>: <span class="hljs-string">'otherId'</span> <span class="hljs-comment">// encodeCursor('Cinema', item['otherId'])</span>
  }
  <span class="hljs-attr">Film</span>: {
    <span class="hljs-attr">cursorPropOrFn</span>: <span class="hljs-function">(<span class="hljs-params">type, item</span>) =&gt;</span> {
      <span class="hljs-comment">// Add multiple properties to the cursor to drive</span>
      <span class="hljs-comment">// multi-value sorting on your DB queries</span>
      <span class="hljs-keyword">return</span> encodeCursor(type, <span class="hljs-string">`<span class="hljs-subst">${item.id}</span>:<span class="hljs-subst">${item.OtherId}</span>`</span>)
    }
  }
}

<span class="hljs-keyword">const</span> { connectionDirectiveTypeDefs, connectionDirectiveTransformer } =
  connectionDirective(typeOptionsMap)
</code></pre>
<h2 id="heading-providing-an-accurate-hasnextpage-value">Providing an accurate "hasNextPage" value</h2>
<p>One of the key drivers of pagination is the ability to know whether you should query for another page. In the Connection Model, this is powered by the <code>pageInfo.hasNextPage</code> value. If you choose not to provide this value, then we'll determine the value based on whether the length of returned nodes matches that of the requested page size. This can be effective in situations where you aren't displaying any UI element to indicate there is a next page. In scenarios where this value needs to be more precise, it can be supplied from your resolver.</p>
<pre><code class="lang-js"><span class="hljs-keyword">const</span> resolvers = {
  <span class="hljs-attr">Query</span>: {
    <span class="hljs-attr">people</span>: <span class="hljs-keyword">async</span> (root, { first, after }, { app }) =&gt; {
      <span class="hljs-keyword">const</span> users = <span class="hljs-keyword">await</span> app.db.getUsers(first + <span class="hljs-number">1</span>, after)
      <span class="hljs-keyword">return</span> {
        <span class="hljs-attr">edges</span>: users,
        <span class="hljs-attr">pageInfo</span>: {
          <span class="hljs-attr">hasNextPage</span>: users.length &gt; first,
        }
      }
    },
  },
}
</code></pre>
<p>When an object is returned by a resolver we will merge the <code>pageInfo</code> object with the library-provided value. The <code>edges</code> property will be transformed to match the <code>Connection</code> schema and any additional properties will be added to the <code>Connection</code> object.</p>
<p>Strategies for determining the <code>hasNextPage</code> value are described in more detail in the <a target="_blank" href="https://github.com/nearform/relay-pagination-directive#hasnextpage-strategies">library documentation</a></p>
<h2 id="heading-try-it-out">Try it out</h2>
<p>GraphQL pagination can be a complex topic, especially for those new to the Graph data way of thinking. Using this directive allows you to implement powerful pagination functionality without making huge changes to your existing setup.</p>
<p>Hopefully from what you've seen here this library will be able to simplify your GraphQL setup. Full details of configuration options and examples of what can be done with this directive are available in the GitHub repository <a target="_blank" href="https://github.com/nearform/relay-pagination-directive">https://github.com/nearform/relay-pagination-directive</a></p>
]]></content:encoded></item></channel></rss>