<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: MyUnisoft</title>
    <description>The latest articles on DEV Community by MyUnisoft (@myunisoft).</description>
    <link>https://dev.to/myunisoft</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F9816%2F84062579-f1f0-4751-b8ba-171afcdbc7ad.png</url>
      <title>DEV Community: MyUnisoft</title>
      <link>https://dev.to/myunisoft</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/myunisoft"/>
    <language>en</language>
    <item>
      <title>Migrating from Nextcloud to Azure S3</title>
      <dc:creator>Thomas.G</dc:creator>
      <pubDate>Tue, 20 May 2025 12:14:44 +0000</pubDate>
      <link>https://dev.to/myunisoft/migrating-from-nextcloud-to-azure-s3-ek6</link>
      <guid>https://dev.to/myunisoft/migrating-from-nextcloud-to-azure-s3-ek6</guid>
      <description>&lt;p&gt;Hello 👋&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Back for a new MyUnisoft technical article&lt;/strong&gt;, this time with the help of my colleague &lt;a href="https://www.linkedin.com/in/nico-mart/" rel="noopener noreferrer"&gt;Nicolas MARTEAU&lt;/a&gt;. Today, we will share our journey to completely refactor our document management architecture and how we migrated from &lt;a href="https://nextcloud.com/fr/" rel="noopener noreferrer"&gt;Nextcloud&lt;/a&gt; to Azure S3 as our storage technology.&lt;/p&gt;

&lt;p&gt;We weren’t able to cover every detail—both for &lt;strong&gt;security&lt;/strong&gt; reasons 🛡️ and to &lt;strong&gt;protect sensitive data&lt;/strong&gt; 🔒—but I hope you will enjoy what I could share. 😊&lt;/p&gt;

&lt;h2&gt;
  
  
  👀 Why moving away from Nextcloud ?
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Performance 🤖
&lt;/h3&gt;

&lt;p&gt;Until now, we have managed several &lt;strong&gt;tens of millions of documents&lt;/strong&gt; with Nextcloud. However, &lt;strong&gt;stability and performance&lt;/strong&gt; had become an issue, with regular &lt;strong&gt;downtime&lt;/strong&gt; 🕒 and &lt;strong&gt;delays&lt;/strong&gt; ⏳ of several minutes for a simple document upload at times.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💬 These upload delays sometimes led to &lt;strong&gt;misunderstandings among users&lt;/strong&gt;. For example, in certain integrations, it was not uncommon for users to delete their Accounting entries 🧾 after a few seconds because they thought the attachment was missing.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The &lt;strong&gt;complexity and limited functionality&lt;/strong&gt; of the existing APIs quickly became a significant obstacle 🛑. Simply making a document available in a specific folder could require four or five separate HTTP requests. We needed a more &lt;strong&gt;robust storage solution&lt;/strong&gt; that could &lt;strong&gt;scale effectively&lt;/strong&gt; 📈 and provide consistent, fast response times. 🚀&lt;/p&gt;

&lt;h3&gt;
  
  
  Infrastructure 🏢
&lt;/h3&gt;

&lt;p&gt;Furthermore, we needed to reduce Nextcloud's impact on our infrastructure. Unlike Azure, Nextcloud &lt;strong&gt;doesn't scale well&lt;/strong&gt; and required &lt;strong&gt;too much maintenance&lt;/strong&gt; from our DevOps team.&lt;/p&gt;

&lt;h2&gt;
  
  
  😬 Architectural issues
&lt;/h2&gt;

&lt;p&gt;In the past, users accessed documents stored directly on Nextcloud, with some files displayed through the platform’s built-in viewers.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdj3cs6unvzu5dyyoev1v.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdj3cs6unvzu5dyyoev1v.png" alt="GED Architecture 1" width="511" height="278"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This initial choice was certainly made for simplicity, but it has evolved into a significant architectural challenge as we began exposing storage directly to customers. Changing a storage server without affecting our users has become complex, and it also complicates the management of certain security and observability concerns.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F68ieabrudi90yf3w2w8z.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F68ieabrudi90yf3w2w8z.png" alt="GED Architecture 2" width="506" height="182"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The primary issue is with PDF documents, such as ledgers, which contain hardcoded URLs pointing to specific storage servers. This requires us to maintain these URLs for years to ensure continued access.&lt;/p&gt;




&lt;p&gt;As part of our migration to S3, we are addressing these issues by routing all requests through the same service (GED).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F796orjevzval17xcgiuh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F796orjevzval17xcgiuh.png" alt="GED New Architecturee" width="800" height="345"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This approach enables us to resolve several issues and enhance the product’s functionality:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Requiring authentication for some sensible documents.&lt;/li&gt;
&lt;li&gt;Providing full observability over who uploads or downloads specific documents.&lt;/li&gt;
&lt;li&gt;Enabling updates to storage capabilities without impacting customers.&lt;/li&gt;
&lt;li&gt;Integrating new storage technologies seamlessly and transparently—for instance, through potential future integrations with services like Microsoft OneDrive.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  📢 The plan
&lt;/h2&gt;

&lt;p&gt;The first step was &lt;strong&gt;to draft an action plan&lt;/strong&gt; 📝 and thoroughly document the existing setup. After several weeks of work, we established the key steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Route all document&lt;/strong&gt; downloads through the GED service.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Route all document&lt;/strong&gt; uploads through the GED service.&lt;/li&gt;
&lt;li&gt;Migrate all existing documents to our new Azure storage, &lt;strong&gt;ensuring zero impact&lt;/strong&gt; 🚫 on the end user.&lt;/li&gt;
&lt;li&gt;Managing the Nextcloud links found in the &lt;strong&gt;PDFs&lt;/strong&gt; already exported by our clients before the migration. Since these links pointed directly to our &lt;strong&gt;Nextcloud servers&lt;/strong&gt;💀, we had to find a reliable solution to route these calls through the GED.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Each stage comes with its own set of &lt;strong&gt;challenges&lt;/strong&gt;, which we’ll examine in detail later in the article.&lt;/p&gt;

&lt;p&gt;Our primary concern, however, was to correct previous architectural missteps 🔍.&lt;/p&gt;

&lt;h3&gt;
  
  
  1️⃣ Download
&lt;/h3&gt;

&lt;p&gt;The first step was to &lt;strong&gt;re-abstract downloads and previews&lt;/strong&gt;, routing them through our backend. This required us to manage both &lt;strong&gt;legacy documents&lt;/strong&gt; 📜 still stored on Nextcloud and &lt;strong&gt;new documents&lt;/strong&gt; that would be hosted on Azure storage.&lt;/p&gt;

&lt;p&gt;One challenge we’re facing is that the token generated by Nextcloud lacks any information about the tenant associated with the document. Without this, our &lt;strong&gt;backend cannot identify the relevant database cluster and tenant&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fuflt844o911ppjqkootm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fuflt844o911ppjqkootm.png" alt="TokenXTenant" width="737" height="483"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To resolve this, we created a new opaque token that embeds the tenant ID:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;node:crypto&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;tenantId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;randomBytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hex&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// =&amp;gt; 1-f82158a508b8bfbed82b601e2ed60edd&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  🔮 Previews
&lt;/h4&gt;

&lt;p&gt;Nextcloud offered automatic previews of uploaded files, a feature we relied on extensively, so we needed to re-implement an equivalent ourselves.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8eng0b0yhrrby67y29wt.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8eng0b0yhrrby67y29wt.jpg" alt="download_preview_usage" width="800" height="258"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We decided not to generate previews at upload, as this would have added significant complexity and cost, along with the challenge of handling asynchronous generation.&lt;/p&gt;

&lt;p&gt;For PDFs, we just return an optimized preview of the first page, and for images, we use the Sharp library.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getImageTransformer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;x&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;y&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="nx"&gt;ext&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;sharp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Sharp&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;y&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getDimensions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;transformer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Azure&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isAlphaImage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ext&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nf"&gt;sharp&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;png&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;sharp&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;failOn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;none&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;jpeg&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;mozjpeg&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;quality&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;transformer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resize&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;fit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;inside&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;withoutEnlargement&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;x&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  📦 Headers and encoding
&lt;/h4&gt;

&lt;p&gt;When returning documents, it’s essential to set the correct HTTP headers and apply proper encoding to values like file names.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;contentType&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;mime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;contentType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;extname&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;contentLength&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getFileFromAzure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// pipe body to reply/response&lt;/span&gt;

&lt;span class="nx"&gt;reply&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Disposition&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s2"&gt;`attachment; filename="&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;encodeURIComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;"`&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;reply&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;contentType&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;reply&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Length&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;contentLength&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I frequently see editors forget to re-inject the file name.&lt;/p&gt;

&lt;h4&gt;
  
  
  Monitoring
&lt;/h4&gt;

&lt;p&gt;Being able to monitor developments and misuse is critical to guaranteeing the stability of our infrastructures.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnuglssormvjq3zgz7pq3.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnuglssormvjq3zgz7pq3.jpg" alt="GED download monitoring" width="785" height="264"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Built-in file viewer
&lt;/h4&gt;

&lt;p&gt;Since Nextcloud could display multiple documents within a viewer, we chose to re-implement a minimal yet functional viewer to retain this capability.&lt;/p&gt;

&lt;p&gt;While our front-ends offer more advanced display modules, this lightweight viewer remains useful in several scenarios:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Replacing or rewriting legacy URLs in PDFs.&lt;/li&gt;
&lt;li&gt;External links shared via APIs.&lt;/li&gt;
&lt;li&gt;Providing quick access for debugging purposes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fa8whimyi25u89t9022zn.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fa8whimyi25u89t9022zn.jpg" alt="GED viewer" width="800" height="322"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  2️⃣ Upload
&lt;/h3&gt;

&lt;p&gt;Successfully prototyping an upload wasn’t as complex as expected… but, as always, the devil is in the details.&lt;/p&gt;

&lt;p&gt;For inter-service uploads between Node applications, another &lt;strong&gt;Fastify plugin&lt;/strong&gt; was added to our workspace package, providing methods to interact with the GED API 🔀.&lt;/p&gt;

&lt;h4&gt;
  
  
  📉 Optimize PDFs and images
&lt;/h4&gt;

&lt;p&gt;Many of the PDFs and images submitted by our users are &lt;strong&gt;quite large and can be optimized&lt;/strong&gt;. For this, we use &lt;a href="https://www.ghostscript.com/" rel="noopener noreferrer"&gt;Ghostscript&lt;/a&gt; 👻 to optimize PDFs and the &lt;a href="https://sharp.pixelplumbing.com/" rel="noopener noreferrer"&gt;Sharp&lt;/a&gt; package for images.&lt;/p&gt;

&lt;p&gt;To date, we’ve reduced the size of received PDFs and images by an average of 50%, with &lt;strong&gt;no loss in quality&lt;/strong&gt; ✨.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftts60uamp0h8qsoxrafv.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftts60uamp0h8qsoxrafv.jpg" alt="GhostscriptSharp" width="264" height="305"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Compression is performed asynchronously using &lt;strong&gt;setImmediate&lt;/strong&gt; to ensure fast server response times.&lt;br&gt;
A compression value of &lt;strong&gt;"null"&lt;/strong&gt; indicates that the compression &lt;strong&gt;ratio is below 5%&lt;/strong&gt; 🤷‍♀️, making the update to the file on Azure negligible.&lt;br&gt;
Otherwise, the file is updated in the cloud ✔.&lt;/p&gt;

&lt;p&gt;Most of these optimizations are carried out via streams, so that the file or image is &lt;strong&gt;never completely buffered&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;However, we needed to remain &lt;strong&gt;vigilant about rising CPU consumption&lt;/strong&gt; and enhance our infrastructure setup 🏗️ to handle increased workloads effectively.&lt;/p&gt;
&lt;h4&gt;
  
  
  🖼️ HEIC/HEIF
&lt;/h4&gt;

&lt;p&gt;Apple's proprietary &lt;strong&gt;HEIC&lt;/strong&gt; format 📱 presented a significant challenge, often requiring conversion to JPG or PNG for compatibility.&lt;/p&gt;

&lt;p&gt;Given that Python bindings to libheif showed much better performance, we initially opted to create our own N-API Node.js binding for libheif, using low-level libraries for rapid JPG and PNG conversion.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/MyUnisoft/heif-converter" rel="noopener noreferrer"&gt;&lt;strong&gt;HEIF-converter&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For maintenance reasons&lt;/strong&gt;, we chose to use &lt;a href="https://www.npmjs.com/package/sharp" rel="noopener noreferrer"&gt;&lt;strong&gt;Sharp&lt;/strong&gt;&lt;/a&gt; by building &lt;strong&gt;libvips&lt;/strong&gt; directly on our machines and installing the &lt;strong&gt;necessary tools&lt;/strong&gt; (libheif, mozjpeg, libpng, etc.).&lt;/p&gt;
&lt;h4&gt;
  
  
  🔒 Security
&lt;/h4&gt;

&lt;p&gt;When managing file uploads and storage, &lt;strong&gt;vigilance is essential&lt;/strong&gt; 🕵️‍♂️ in several areas:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Monitor for &lt;strong&gt;spoofed HTTP headers&lt;/strong&gt; 🛡️, such as altered content-type headers.&lt;/li&gt;
&lt;li&gt;Scan files for &lt;strong&gt;viruses&lt;/strong&gt; and &lt;strong&gt;malicious content&lt;/strong&gt; 🦠.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Otherwise, an attacker could misuse your brand and storage capabilities to &lt;strong&gt;distribute malicious content and compromise users&lt;/strong&gt; 🚨.&lt;/p&gt;

&lt;p&gt;Make it a habit to consult the OWASP cheat sheets to ensure maximum protection against errors and oversights: &lt;a href="https://cheatsheetseries.owasp.org/cheatsheets/File_Upload_Cheat_Sheet.html" rel="noopener noreferrer"&gt;OWASP File Upload Cheat Sheet&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;We used &lt;a href="https://www.npmjs.com/package/clamscan" rel="noopener noreferrer"&gt;&lt;strong&gt;clamscan&lt;/strong&gt;&lt;/a&gt; (which relies on &lt;strong&gt;ClamAV&lt;/strong&gt;) to scan the files 👁️, and &lt;a href="https://www.npmjs.com/package/file-type" rel="noopener noreferrer"&gt;&lt;strong&gt;file-type&lt;/strong&gt;&lt;/a&gt; to accurately identify the file type instead of relying solely on the request headers 🧨.&lt;/p&gt;
&lt;h4&gt;
  
  
  📊 Monitoring
&lt;/h4&gt;

&lt;p&gt;As we regain control, it’s essential not to overlook usage monitoring through logs and other metrics.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fs2zktzujkrg4nosnfx89.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fs2zktzujkrg4nosnfx89.jpg" alt="myunisoft_ged_upload_monitoring_1" width="393" height="528"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  3️⃣ Migrating Nextcloud documents
&lt;/h3&gt;

&lt;p&gt;To gradually phase out our Nextcloud servers, we developped a &lt;strong&gt;temporary Node.js API&lt;/strong&gt; 🦾, responsible for transferring resources from Nextcloud to Azure. This service handled upload concurrency, which we've limited to &lt;strong&gt;64 simultaneous uploads&lt;/strong&gt; to avoid overloading the server 🔥.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5g4ou6sg63af27nhfbs3.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5g4ou6sg63af27nhfbs3.jpg" alt="Albatros_tool" width="800" height="220"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Without detailing every feature of this internal tool, it was designed to support key functionalities such as pausing and resuming the migration process, as well as monitoring the status of each transfers (&lt;strong&gt;successes&lt;/strong&gt; ✅, &lt;strong&gt;errors&lt;/strong&gt; ❌, &lt;strong&gt;totals&lt;/strong&gt;, etc.).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0kn8hmvo1clrlefptp4e.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0kn8hmvo1clrlefptp4e.png" alt="Nextcloud_2_totals" width="215" height="120"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h4&gt;
  
  
  &lt;strong&gt;Step 1&lt;/strong&gt;: Data Extraction
&lt;/h4&gt;

&lt;p&gt;We extracted from the Nextcloud database all the tokens 📄 (used to retrieve document data from the database) and the file paths on the server (to transfer the resources), saving them into &lt;code&gt;.csv&lt;/code&gt; or &lt;code&gt;.txt&lt;/code&gt; files.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fp0uwdx6fyv4qcu7tmanp.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fp0uwdx6fyv4qcu7tmanp.jpg" alt="Nextcloud_export" width="800" height="56"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h4&gt;
  
  
  &lt;strong&gt;Step 2&lt;/strong&gt;: Environment Setup
&lt;/h4&gt;

&lt;p&gt;We then set up a &lt;strong&gt;NAS server&lt;/strong&gt; to run the Node.js tool and directly access the file system 🦄, bypassing the Nextcloud API. This approach was chosen to maximize performance and enable efficient stream-based, parallel processing of the document transfers.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2hyqnytulu9gh5rkokev.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2hyqnytulu9gh5rkokev.jpg" alt="Nextcloud_NAS" width="800" height="407"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h4&gt;
  
  
  &lt;strong&gt;Step 3&lt;/strong&gt;: Create the DB and go 🧨
&lt;/h4&gt;

&lt;p&gt;All that remained was to create the &lt;strong&gt;SQLite databases&lt;/strong&gt; (we chose to generate one database per firm to avoid excessively large files), using the Nextcloud exports that contained &lt;strong&gt;tens of millions of rows&lt;/strong&gt;, and then start the transfers ✅.&lt;/p&gt;

&lt;p&gt;Let’s just say we ran into a few surprises along the way 🤫, and the migration ended up taking us several days 🤭.&lt;/p&gt;
&lt;h3&gt;
  
  
  4️⃣ Legacy URLs in PDF
&lt;/h3&gt;

&lt;p&gt;Some &lt;strong&gt;URLs are permanently embedded in PDFs&lt;/strong&gt; 📄, so we need to consider strategies for rewriting them using the information available.&lt;/p&gt;

&lt;p&gt;Since Nextcloud tokens didn’t contain any tenant information, we created a &lt;strong&gt;minimal API&lt;/strong&gt; (microservice) supported by an &lt;strong&gt;SQLite&lt;/strong&gt; database to maintain the relationship between a token and its corresponding tenant ID.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;PRAGMA&lt;/span&gt; &lt;span class="n"&gt;journal_mode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;OFF&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;PRAGMA&lt;/span&gt; &lt;span class="n"&gt;synchronous&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;PRAGMA&lt;/span&gt; &lt;span class="n"&gt;locking_mode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;EXCLUSIVE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="nv"&gt;"tokens"&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nv"&gt;"token"&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nv"&gt;"schema"&lt;/span&gt; &lt;span class="nb"&gt;INTEGER&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;WITHOUT&lt;/span&gt; &lt;span class="n"&gt;ROWID&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We can manage thousands of tokens within just a &lt;strong&gt;few milliseconds&lt;/strong&gt; ⏱️ using purely synchronous I/O. Additionally, we implemented an &lt;strong&gt;LRU cache&lt;/strong&gt; to ensure that repetitive requests are handled even more quickly.&lt;/p&gt;

&lt;p&gt;The final step is to configure HAProxy 🔀 to &lt;strong&gt;redirect nextcloud viewer requests&lt;/strong&gt; to a specific &lt;strong&gt;GED endpoint&lt;/strong&gt;, where the URL is parsed to retrieve tokens and correlate them with their respective tenants, using the project setup described above.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;link&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;canParse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;link&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Problem&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="c1"&gt;// Other URL validation here&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tokens&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;link&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;(?&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;=&lt;/span&gt;&lt;span class="se"&gt;\/)([&lt;/span&gt;&lt;span class="sr"&gt;1-9&lt;/span&gt;&lt;span class="se"&gt;]{1,4}&lt;/span&gt;&lt;span class="sr"&gt;-&lt;/span&gt;&lt;span class="se"&gt;\w{15,32}&lt;/span&gt;&lt;span class="sr"&gt;|&lt;/span&gt;&lt;span class="se"&gt;\w{15})(?=\W&lt;/span&gt;&lt;span class="sr"&gt;|$&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="sr"&gt;/g&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// Correlate tokens with our microservice database&lt;/span&gt;

&lt;span class="nx"&gt;reply&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`/ged/document/view?tokens=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;correlatedTokens&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;|&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is only a partial overview of the implementation. We use a combination of the WHATWG URL API and regular expressions to extract tokens, ensuring sufficient security to mitigate any ReDoS attack vectors.&lt;/p&gt;

&lt;p&gt;We then redirect the request with all tokens to our built-in viewer.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4oczy3memfuj2m9stg2t.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4oczy3memfuj2m9stg2t.jpg" alt="Built-in_viewer" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  🔬 What we've learned
&lt;/h2&gt;

&lt;p&gt;This project taught us that errors in URLs saved within PDF documents are hard to forgive. Due to some technical debt and a lack of foresight, we ended up with an unintended &lt;code&gt;/ged/ged&lt;/code&gt; prefix. Today we're having a bit of a laugh about it, and if you see this prefix you'll know it wasn't meant to be 😆.&lt;/p&gt;

&lt;p&gt;Managing files with proper streaming while handling errors proved far more challenging than anticipated, plaguing us for weeks with ghost files, memory leaks, and other unexpected bugs. At this level of usage, it’s technical excellence or nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  ❤️ Credits
&lt;/h2&gt;

&lt;p&gt;A migration project of &lt;strong&gt;this scale doesn’t happen overnight&lt;/strong&gt;—it took us well over a year to complete all the steps outlined above. A big thank you 🙏 to everyone involved for their dedication and effort ❤️.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Nicolas 👨‍💻, for leading the project development from &lt;strong&gt;A to Z&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;The infrastructure team 🏗️ (Vincent, Jean-Charles, and Cyril) for their &lt;strong&gt;consistent support&lt;/strong&gt; throughout the project.&lt;/li&gt;
&lt;li&gt;Aymeric, for managing and leading the migration of downloads and uploads for his team services.&lt;/li&gt;
&lt;li&gt;Many others 👥 for their &lt;strong&gt;reviews and support&lt;/strong&gt; 📝.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This project was incredibly rewarding 🏆, both for its &lt;strong&gt;challenges&lt;/strong&gt; and the range of &lt;strong&gt;architectural issues it addressed&lt;/strong&gt; 📐.&lt;/p&gt;




&lt;p&gt;Thank you, see you soon for another technical adventure 😉😊&lt;/p&gt;

&lt;p&gt;👋👋👋&lt;/p&gt;

</description>
      <category>node</category>
      <category>nextcloud</category>
      <category>azure</category>
      <category>s3</category>
    </item>
    <item>
      <title>Designing MyUnisoft Next-Gen Accounting APIs</title>
      <dc:creator>Thomas.G</dc:creator>
      <pubDate>Thu, 04 Apr 2024 14:20:41 +0000</pubDate>
      <link>https://dev.to/myunisoft/designing-myunisoft-next-gen-accounting-apis-1mn</link>
      <guid>https://dev.to/myunisoft/designing-myunisoft-next-gen-accounting-apis-1mn</guid>
      <description>&lt;p&gt;Hello 👋&lt;/p&gt;

&lt;p&gt;I'm excited to present my latest technical article detailing the design and development process behind the &lt;strong&gt;next&lt;/strong&gt; iteration of the MyUnisoft Set of accounting external APIs.&lt;/p&gt;

&lt;p&gt;Internally known as &lt;strong&gt;MAD&lt;/strong&gt; for MyUnisoft Accounting Data 😄, this project showcases our team's innovative approach and dedication to enhancing API and Partners experience.&lt;/p&gt;

&lt;h2&gt;
  
  
  💡 Context about the business requirements
&lt;/h2&gt;

&lt;p&gt;You might be questioning the need for developing new APIs. We embarked on this effort to address a spectrum of issues and fulfill unmet needs, including:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🔄 Introducing a modern and efficient format for &lt;strong&gt;exporting and importing accounting folders&lt;/strong&gt;, along with their parameters (particularly useful for migrations between tenants).&lt;/li&gt;
&lt;li&gt;📊 Incorporating accounting data that was previously absent from historical partners APIs, such as &lt;strong&gt;Analytics&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;🚀 Constructing &lt;strong&gt;high-performance&lt;/strong&gt;, &lt;strong&gt;user-friendly&lt;/strong&gt; APIs for our partners, adhering to &lt;strong&gt;modern API best practices&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When I refer to &lt;strong&gt;high performance&lt;/strong&gt;, I'm not solely emphasizing faster response times. It also includes improving the security and strength of our systems by using &lt;strong&gt;cache&lt;/strong&gt; and &lt;strong&gt;queues&lt;/strong&gt;, as well as ensuring &lt;strong&gt;better observability&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  👻 TRA
&lt;/h3&gt;

&lt;p&gt;Historically, the &lt;strong&gt;&lt;a href="https://www.cegid.com/fr/" rel="noopener noreferrer"&gt;CEGID TRA format&lt;/a&gt;&lt;/strong&gt; has been predominant in the French market, but it faces a number of issues:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Tailored for CEGID's specific needs, many columns within the format prove &lt;strong&gt;irrelevant for MyUnisoft's purposes&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Featuring a column-oriented structure with fixed positions, it becomes &lt;strong&gt;resistant to evolution&lt;/strong&gt; and challenging to implement and maintain 😡.&lt;/li&gt;
&lt;li&gt;Parsing and generating data in this format incur significant &lt;strong&gt;costs and complexities&lt;/strong&gt; 😞.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;However, it can be difficult for software editors and accounting firms to adapt to unfamiliar formats quickly. That's why we've decided to be compatible with TRA.&lt;/p&gt;

&lt;p&gt;The basic idea is to enable &lt;strong&gt;faster integration and smoother migration&lt;/strong&gt; to our format. If this format has worked so far, we can learn a lot from it 😊.&lt;/p&gt;

&lt;h3&gt;
  
  
  💪 Accounting imputation for Factur-X
&lt;/h3&gt;

&lt;p&gt;A significant disappointment lies in the realization that simply exchanging &lt;strong&gt;Factur-X via APIs&lt;/strong&gt; will prove largely inadequate for creating a satisfactory experience for accounting editors and their customers (CPA), as the format does not permit embedding data for &lt;strong&gt;accounting imputation&lt;/strong&gt; 😞.&lt;/p&gt;

&lt;p&gt;To address this issue, we are planning to implement &lt;strong&gt;Factur-X natively within MAD&lt;/strong&gt;. This format will enable the fields to be omitted, provided that the attached file is a valid Factur-X. Developers will only need to fill in the required fields for imputation purposes.&lt;/p&gt;

&lt;p&gt;🚫 There will be no necessity for creating &lt;strong&gt;non-standard extensions of the XML format&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  ✒️ Technical specification
&lt;/h2&gt;

&lt;p&gt;From the beginning, my aim was to develop accounting APIs that incorporate the following features: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Versioned interface contracts&lt;/strong&gt; to establish lifecycles and facilitate iterative changes more smoothly.&lt;/li&gt;
&lt;li&gt;Utilization of &lt;strong&gt;identical interfaces&lt;/strong&gt; for both import and export functions, thereby simplifying API interactions.&lt;/li&gt;
&lt;li&gt;🧠 Maximizing intelligence and independence to minimize dependency on other APIs. For instance, this includes eliminating the necessity for IDs or internal codes during imports.&lt;/li&gt;
&lt;li&gt;Striving for &lt;strong&gt;neutrality&lt;/strong&gt; when crafting the format, ensuring compatibility and flexibility across various systems and use cases.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Lastly, it's imperative to highlight the significance of neutrality in our approach. Too often, technical teams can fall into the trap of designing formats and APIs solely with internal use cases in mind, often under the pressure of immediate business requirements.&lt;/p&gt;

&lt;p&gt;However, it's essential to recognize that our target audience primarily comprises developers who experience accounting through the lens of their own unique business contexts. By prioritizing neutrality in our design principles, we aim to simplify the lives of our consumers and empower them with flexible solutions that seamlessly adapt to diverse workflows.&lt;/p&gt;

&lt;p&gt;Through MAD, we're committed to fostering an ecosystem that prioritizes &lt;strong&gt;developer-centric&lt;/strong&gt; design, ensuring that our APIs serve as versatile tools capable of meeting a wide range of needs 🌐.&lt;/p&gt;

&lt;h3&gt;
  
  
  🔍 Gathering information on existing assets
&lt;/h3&gt;

&lt;p&gt;The initial step involved conducting an inventory of existing APIs and comprehensively understanding the data consumed by our partners. We didn't have to do a lot of work, given that we have a &lt;strong&gt;nice &lt;a href="https://partners.api.myunisoft.fr/" rel="noopener noreferrer"&gt;public documentation&lt;/a&gt; available&lt;/strong&gt; 😎!&lt;/p&gt;

&lt;p&gt;We reviewed the APIs of several of our competitors, complemented by our experience integrating with various accounting editors, which guided us in making the right design decisions.&lt;/p&gt;

&lt;p&gt;Additionally, we meticulously analyzed our &lt;strong&gt;TRA backend&lt;/strong&gt; to extract the required columns for a preliminary version. This enabled us to identify the &lt;strong&gt;essential elements for a version 1&lt;/strong&gt;, including:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;accounting folder&lt;/li&gt;
&lt;li&gt;payments&lt;/li&gt;
&lt;li&gt;banks&lt;/li&gt;
&lt;li&gt;journals&lt;/li&gt;
&lt;li&gt;exercices&lt;/li&gt;
&lt;li&gt;analytics (axes and their sections)&lt;/li&gt;
&lt;li&gt;entries &amp;amp; movements&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;🔍 Our plan is to expand this list to incorporate more elements over time.&lt;/p&gt;

&lt;h3&gt;
  
  
  📚 Building interfaces
&lt;/h3&gt;

&lt;p&gt;The second step involved crafting prototypes for our JSON object interfaces, utilizing &lt;strong&gt;TypeScript&lt;/strong&gt; and &lt;strong&gt;JSON Schema&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Here is the finalized version for &lt;strong&gt;Journal&lt;/strong&gt;, which underwent several weeks of iteration, concurrent with the creation of initial drafts of SQL.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;SimplifiedAccount&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;producerId&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;number&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;TypeJournal&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Achat&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Vente&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Banque&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;Journal&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;producerId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;customerReferenceCode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TypeJournal&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;counterpartAccount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SimplifiedAccount&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;additionalProducerProperties&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TypeJournalInternal&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;locked&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;💡 Properties that are too specific to MyUnisoft are stored in &lt;strong&gt;additionalProducerProperties&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Simultaneously iterating on the SQL has proven essential, allowing us to eliminate keys that significantly impact performance without providing &lt;strong&gt;commensurate value&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Here is an another example with &lt;strong&gt;Bank&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;Bank&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;producerId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;IBAN&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;BIC&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;account&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SimplifiedAccount&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;journal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;producerId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;BANQUE&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;customerReferenceCode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="nl"&gt;additionalProducerProperties&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;isDefault&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A significant effort has been invested in creating objects that are both clean and user-friendly.&lt;/p&gt;

&lt;h3&gt;
  
  
  🌀 (Bonus) Known your abstraction
&lt;/h3&gt;

&lt;p&gt;We frequently fall into the trap of assuming that our abstractions are universally understood across a entire ecosystem. In accounting, for instance, it's typical to categorize transactions into an abstraction known as an "Entry." Each entry comprises a set of balanced movements (or transactions).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6g7ozjlri6w20sykm1qn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6g7ozjlri6w20sykm1qn.png" alt="entry/movement abstract" width="557" height="379"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;However, many software solutions, including &lt;strong&gt;Dataviz&lt;/strong&gt;, often prefer consuming &lt;strong&gt;raw movements without additional abstractions&lt;/strong&gt;. This preference explains why such solutions historically lean towards formats like FEC, which are simpler compared to TRA-heavy formats.&lt;/p&gt;

&lt;p&gt;From my experience working with partners over the years, I've learned that many of them employ various abstractions for VAT, accounts, or analytics. Interestingly, the choice of abstraction can sometimes be influenced by the &lt;strong&gt;nature of their software&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The way we structure our data can play a vital role in ensuring a positive experience for specific consumers 💡.&lt;/p&gt;

&lt;h2&gt;
  
  
  💬 Technical implementation
&lt;/h2&gt;

&lt;p&gt;For this project we decided to create an &lt;strong&gt;in-house package&lt;/strong&gt; that would bring together all the necessary parts. This decision primarily aims to accommodate diverse use cases &lt;strong&gt;without the need for repetitive API&lt;/strong&gt; (or SDK) integration.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5eqn0gj6vvjdl9a89489.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5eqn0gj6vvjdl9a89489.png" alt="MAD Usage" width="678" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The second step was to design a &lt;strong&gt;testable architecture&lt;/strong&gt; for the package. For some parts I drew inspiration from open source libs such as &lt;a href="https://undici.nodejs.org/" rel="noopener noreferrer"&gt;undici&lt;/a&gt; and their mocking API.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7pv06vgwg6x5agt30iye.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7pv06vgwg6x5agt30iye.png" alt="MAD Package" width="800" height="351"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;At one point I was thinking that it might be a problem to implement &lt;a href="https://fr.wikipedia.org/wiki/HATEOAS#:~:text=HATEOAS%2C%20abr%C3%A9viation%20d'Hypermedia%20As,autres%20architectures%20d'applications%20r%C3%A9seau." rel="noopener noreferrer"&gt;HATEOAS&lt;/a&gt;... However, I discovered that with our architecture, dynamically injecting new links to specific resources using transformers is surprisingly straightforward 😅.&lt;/p&gt;

&lt;h3&gt;
  
  
  📦 Package usage
&lt;/h3&gt;

&lt;p&gt;Here's a simple example&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;MAD&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@myunisoft/mad&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;exporter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;MAD&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Export&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;1.x&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;accountingFolderId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;10&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;postgresDataSource&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;exercices&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;exporter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exercices&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Underneath we fetch elements from the database and transform them into the correct format, leveraging the &lt;strong&gt;SemVer version&lt;/strong&gt; injected into the constructor.&lt;/p&gt;

&lt;p&gt;Furthermore, we've integrated an observability layer to track the execution times of each component. However, this aspect likely requires further refinement 😕.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;exercices&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;ExportResponse&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;postgre&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Exercice&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;startTime&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;performance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rawExercices&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exercices&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sqlTime&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;performance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;startTime&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;transformed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;transformer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exercices&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rawExercices&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;transformed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;executionTime&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;sqlTime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;transformer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;transformed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;executionTime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;total&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;performance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;startTime&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The provider can &lt;strong&gt;simply be mocked up&lt;/strong&gt; if you need to run unit tests (by forcing a global provider or forcing it using the class constructor).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;kGlobalDispatcher&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;mock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="nf"&gt;before&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;MAD&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setGlobalProvider&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;kGlobalDispatcher&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nf"&gt;beforeEach&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;kGlobalDispatcher&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resetCalls&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nf"&gt;after&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;MAD&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resetGlobalProvider&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  (&lt;strong&gt;Bonus&lt;/strong&gt;) TRA compatibility
&lt;/h3&gt;

&lt;p&gt;Effectively writing or reading a &lt;strong&gt;TRA&lt;/strong&gt; isn't inherently complex, although it can be quite tedious due to the time-consuming process of properly implementing all its sections.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsp8x00rn2jzj7fxt0xwz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsp8x00rn2jzj7fxt0xwz.png" alt="TRA" width="408" height="781"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The challenge lies in being as fast as possible&lt;/strong&gt; ⚡.&lt;/p&gt;

&lt;p&gt;After one or two months of work, our JavaScript implementation can now read and write a complete TRA of several million lines in just a few &lt;strong&gt;hundred milliseconds&lt;/strong&gt; 📈💻.&lt;/p&gt;

&lt;p&gt;To achieve this, we've minimized the number of operations and iterations for each section. Additionally, each section is equipped with a comprehensive definition containing all column information, such as position, alignment, and length.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TRAConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;section&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;rib&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;JSONToTRA&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;TRAToJSON&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;columns&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;fixe&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;mandatory&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;a&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;length&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;align&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;left&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;identifiant&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;mandatory&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;a&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;length&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;align&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;left&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="c1"&gt;// ... other fields&lt;/span&gt;
  &lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each section is equipped with functions designed for &lt;strong&gt;efficient conversion&lt;/strong&gt; between TRA and JSON formats.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;JSONToTRA&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;lineObj&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;IRib&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TRAParsingOptions&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nf"&gt;convertJSONValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;columns&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nx"&gt;lineObj&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fixe&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nf"&gt;convertJSONValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;columns&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nx"&gt;lineObj&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;identifiant&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nf"&gt;convertJSONValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;columns&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nx"&gt;lineObj&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;auxiliaire&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="c1"&gt;// other fields..&lt;/span&gt;
    &lt;span class="nx"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;EOL&lt;/span&gt;
  &lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We utilized tools like flamegraphs to pinpoint the longest-running elements and effectively eliminate them. Additionally, we optimized our process by implementing caching mechanisms to avoid redundant operations, such as repeatedly parsing the same dates.&lt;/p&gt;

&lt;p&gt;Exploring a Rust implementation could be intriguing to further reduce execution time 🚀. Perhaps in &lt;strong&gt;the future&lt;/strong&gt;, if performance becomes a critical concern 👀.&lt;/p&gt;

&lt;h2&gt;
  
  
  🌏 API
&lt;/h2&gt;

&lt;p&gt;In parallel with the work done on the MAD package, &lt;strong&gt;we drew up a plan for creating the APIs&lt;/strong&gt; 📝.&lt;/p&gt;

&lt;p&gt;However, we encountered a challenge: some of the APIs we'll be exposing are potentially risky ⚠️, as they may permit exporting entire folders along with all associated parameters.&lt;/p&gt;

&lt;p&gt;To address this, &lt;strong&gt;we're designing asynchronous APIs&lt;/strong&gt;, incorporating status tracking and caching mechanisms integrated with our event architecture. This setup enables us to trigger webhooks as needed, enhancing security and reliability.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flut8msq5cbaaezt7ep7z.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flut8msq5cbaaezt7ep7z.png" alt="MAD API" width="800" height="1188"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The sequence diagram above illustrates the interactions between the various components involved in exporting an entire accounting folder.&lt;/p&gt;

&lt;p&gt;For this export, we have &lt;strong&gt;two&lt;/strong&gt; distinct routes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;mad/all?format=json&amp;amp;version=1.0.0&lt;/code&gt; to initialize the export.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;mad/all/status?exportId=export_sch-5_acf-1&lt;/code&gt; to fetch the status.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The second route will return a DONE status when processing is complete (Real-time webhook notification can be subscribed to as an option).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"export_sch-1_acf-1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"DONE"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;url&amp;gt;"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  📜 Documentation
&lt;/h2&gt;

&lt;p&gt;We've seamlessly extended our documentation, which is readily accessible on Github. Recently, we've enhanced the user experience by incorporating an online &lt;a href="https://vitepress.dev/" rel="noopener noreferrer"&gt;Vitepress&lt;/a&gt; site, hosted with Github Pages.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3fa2fin8qf8ktaimsido.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3fa2fin8qf8ktaimsido.png" alt="MAD Doc" width="800" height="526"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It required several weeks of effort, and the pivotal elements include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;📝 A comprehensive description outlining our decisions.&lt;/li&gt;
&lt;li&gt;📋 A detailed specification encompassing all interfaces, schemas, etc.&lt;/li&gt;
&lt;li&gt;📚 Additional insights, explanations, and guidance tailored for beginners in accounting.&lt;/li&gt;
&lt;li&gt;🗺️ Instructions on navigating MyUnisoft for developers unfamiliar with our product.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;My primary objective is to ensure a &lt;strong&gt;positive developer experience&lt;/strong&gt; (DX) while maintaining thorough and accessible documentation. You're welcome to explore it yourself &lt;a href="https://partners.api.myunisoft.fr/MAD/introduction.html" rel="noopener noreferrer"&gt;here&lt;/a&gt;, and we eagerly await your feedback 😊.&lt;/p&gt;

&lt;h2&gt;
  
  
  📆 What's next?
&lt;/h2&gt;

&lt;p&gt;We're only at the very beginning of the project. We will continue to add new content to our export format and work on new options for our various APIs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;In parallel, we're working on the import&lt;/strong&gt; (I'll probably have a chance to write a new article when we're well advanced on this).&lt;/p&gt;

&lt;h2&gt;
  
  
  ❤️ Credits
&lt;/h2&gt;

&lt;p&gt;This project is primarily a &lt;strong&gt;collaborative effort&lt;/strong&gt;, made possible by the contributions of key team members:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Benoit GARIAZZO (Product Owner)&lt;/li&gt;
&lt;li&gt;Alexandre MALAJ (Specification, MAD package &amp;amp; SQL).&lt;/li&gt;
&lt;li&gt;Cédric LIONNET (API)&lt;/li&gt;
&lt;li&gt;Pierre DEMAILLY (TRA/JSON compatibility &amp;amp; review)&lt;/li&gt;
&lt;li&gt;Frédéric GUIOU (Help with naming and currently leading import)&lt;/li&gt;
&lt;li&gt;Me 😁 (Specification, Observability, Documentation)&lt;/li&gt;
&lt;li&gt;Léon and Aymeric for their invaluable assistance with existing accounting APIs &amp;amp; formats.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And all other team members who provided valuable feedback and support during the implementation process 🙌.&lt;/p&gt;

&lt;p&gt;🙏 That concludes this article. Thank you for reading!&lt;/p&gt;

</description>
      <category>accounting</category>
      <category>javascript</category>
      <category>node</category>
      <category>api</category>
    </item>
    <item>
      <title>Consuming Loki logs with Grafana API and Node.js</title>
      <dc:creator>Thomas.G</dc:creator>
      <pubDate>Wed, 25 Oct 2023 10:28:24 +0000</pubDate>
      <link>https://dev.to/myunisoft/consuming-loki-logs-with-grafana-api-and-nodejs-bgg</link>
      <guid>https://dev.to/myunisoft/consuming-loki-logs-with-grafana-api-and-nodejs-bgg</guid>
      <description>&lt;p&gt;Hello 👋&lt;/p&gt;

&lt;p&gt;In my last article we were discussing how me and my team had set up and built dashboards with Grafana and Loki 😎.&lt;/p&gt;


&lt;div class="ltag__link"&gt;
  &lt;a href="/myunisoft" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__org__pic"&gt;
      &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F9816%2F84062579-f1f0-4751-b8ba-171afcdbc7ad.png" alt="MyUnisoft" width="225" height="225"&gt;
      &lt;div class="ltag__link__user__pic"&gt;
        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F314815%2F128a0b56-a103-4bc8-92b6-ce3738e98770.jpg" alt="" width="400" height="400"&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/a&gt;
  &lt;a href="https://dev.to/myunisoft/logs-monitoring-with-loki-nodejs-and-fastifyjs-3h8k" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__content"&gt;
      &lt;h2&gt;Logs monitoring with Loki, Node.js and Fastify.js&lt;/h2&gt;
      &lt;h3&gt;Thomas.G for MyUnisoft ・ Jun 12 '23&lt;/h3&gt;
      &lt;div class="ltag__link__taglist"&gt;
        &lt;span class="ltag__link__tag"&gt;#node&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#javascript&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#monitoring&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#grafana&lt;/span&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/a&gt;
&lt;/div&gt;


&lt;p&gt;Today we're going to discuss how you can leverage your logs for third-party tools using Grafana's API and Node.js 😍.&lt;/p&gt;

&lt;h2&gt;
  
  
  💡 The idea that starts it all
&lt;/h2&gt;

&lt;p&gt;Several months ago, I started thinking about how to use our logs to build a personalized CLI experience.&lt;/p&gt;

&lt;p&gt;After a bit of searching, I quickly found that Grafana had an &lt;a href="https://grafana.com/docs/loki/latest/reference/api/" rel="noopener noreferrer"&gt;API for queryings Loki logs&lt;/a&gt;. I immediately went crazy at the thought of what could be done with it 🔥.&lt;/p&gt;

&lt;h2&gt;
  
  
  🐢🚀 Node.js SDK
&lt;/h2&gt;

&lt;p&gt;Over the weekend, I set to create an &lt;a href="https://github.com/MyUnisoft/loki" rel="noopener noreferrer"&gt;open source Node.js SDK&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;GrafanaLoki&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@myunisoft/loki&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;GrafanaLoki&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="c1"&gt;// Note: if not provided, it will load process.env.GRAFANA_API_TOKEN&lt;/span&gt;
  &lt;span class="na"&gt;apiToken&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;...&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;remoteApiURL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://name.loki.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;logs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;queryRange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="s2"&gt;`{app="serviceName", env="production"}`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;start&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;1d&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;logs&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;blockquote&gt;
&lt;p&gt;👀 Notice the support of duration for options like &lt;strong&gt;start&lt;/strong&gt; and &lt;strong&gt;end&lt;/strong&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It already supports Stream and Matrix and several other APIs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;GET /loki/api/v1/labels&lt;/li&gt;
&lt;li&gt;GET /loki/api/v1/label/:name/values&lt;/li&gt;
&lt;li&gt;GET /loki/api/v1/series&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We've also added datasource APIs (which may be required depending on what you'r building).&lt;/p&gt;

&lt;p&gt;It also include a LogParser inspired by Loki pattern.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// can be provided as an option to queryRange&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;parser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;LogParser&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;statusCode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;method:httpMethod&amp;gt; &amp;lt;endpoint&amp;gt; &amp;lt;statusCode:httpStatusCode&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;logs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;queryRange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="s2"&gt;`... LogQL here ...`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;parser&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;logLine&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;logs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;logLine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;method&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;logLine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;It's still far from perfect, don't hesitate to contribute 💪.&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.dev.to%2Fassets%2Fgithub-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/OpenAlly" rel="noopener noreferrer"&gt;
        OpenAlly
      &lt;/a&gt; / &lt;a href="https://github.com/OpenAlly/loki" rel="noopener noreferrer"&gt;
        loki
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Node.js Loki SDK
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;
  Loki
&lt;/h1&gt;
&lt;/div&gt;

&lt;p&gt;
  Node.js Grafana API SDK (Loki, Datasources ..)
&lt;/p&gt;

&lt;p&gt;
    &lt;a href="https://github.com/OpenAlly/loki" rel="noopener noreferrer"&gt;
      &lt;img src="https://camo.githubusercontent.com/0552eef223cb95be8e894b9d73f49e9da3b1a68f945186f4de999266941cf75c/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f7061636b6167652d6a736f6e2f762f4f70656e416c6c792f6c6f6b693f7374796c653d666f722d7468652d6261646765" alt="npm version"&gt;
    &lt;/a&gt;
    &lt;a href="https://github.com/OpenAlly/loki" rel="noopener noreferrer"&gt;
      &lt;img src="https://camo.githubusercontent.com/207eed5c41f8c5433e055e9c73164c5b9a09852eac92d9e381e1f3114eb57b0c/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f6c6963656e73652f4f70656e416c6c792f6c6f6b693f7374796c653d666f722d7468652d6261646765" alt="license"&gt;
    &lt;/a&gt;
    &lt;a href="https://api.securityscorecards.dev/projects/github.com/OpenAlly/loki" rel="nofollow noopener noreferrer"&gt;
      &lt;img src="https://camo.githubusercontent.com/ef5c65274aec81c9b5fe7fce83e5a74e7bbac5352426d7808d8b31056b113821/68747470733a2f2f6170692e736563757269747973636f726563617264732e6465762f70726f6a656374732f6769746875622e636f6d2f4f70656e416c6c792f6c6f6b692f62616467653f7374796c653d666f722d7468652d6261646765" alt="ossf scorecard"&gt;
    &lt;/a&gt;
    &lt;a href="https://github.com/OpenAlly/loki/actions?query=workflow%3A%22Node.js+CI%22" rel="noopener noreferrer"&gt;
      &lt;img src="https://camo.githubusercontent.com/0eb39842628e6fbe576895e909db360abf7c48dc23983c6057ea884c513058ab/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f616374696f6e732f776f726b666c6f772f7374617475732f4f70656e416c6c792f6c6f6b692f6e6f64652e6a732e796d6c3f7374796c653d666f722d7468652d6261646765" alt="github ci workflow"&gt;
    &lt;/a&gt;
    &lt;a href="https://github.com/OpenAlly/loki" rel="noopener noreferrer"&gt;
      &lt;img src="https://camo.githubusercontent.com/26cb2e27ce29b93b62ae1bf0e2db3b8291f346ee746c1d7d8f7ad73b550ef0ee/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f6c616e6775616765732f636f64652d73697a652f4f70656e416c6c792f6c6f6b693f7374796c653d666f722d7468652d6261646765" alt="size"&gt;
    &lt;/a&gt;
&lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;🚧 Requirements&lt;/h2&gt;
&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://nodejs.org/en/" rel="nofollow noopener noreferrer"&gt;Node.js&lt;/a&gt; version 24 or higher&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;🚀 Getting Started&lt;/h2&gt;

&lt;/div&gt;

&lt;p&gt;This package is available in the Node Package Repository and can be easily installed with &lt;a href="https://doc.npmjs.com/getting-started/what-is-npm" rel="nofollow noopener noreferrer"&gt;npm&lt;/a&gt; or &lt;a href="https://yarnpkg.com" rel="nofollow noopener noreferrer"&gt;yarn&lt;/a&gt;&lt;/p&gt;

&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;$ npm i @openally/loki
&lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; or&lt;/span&gt;
$ yarn add @openally/loki&lt;/pre&gt;

&lt;/div&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;📚 Usage&lt;/h2&gt;

&lt;/div&gt;

&lt;div class="highlight highlight-source-ts notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;&lt;span class="pl-k"&gt;import&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt; &lt;span class="pl-v"&gt;GrafanaApi&lt;/span&gt; &lt;span class="pl-kos"&gt;}&lt;/span&gt; &lt;span class="pl-k"&gt;from&lt;/span&gt; &lt;span class="pl-s"&gt;"@openally/loki"&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
&lt;span class="pl-k"&gt;import&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt; &lt;span class="pl-v"&gt;LogQL&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-v"&gt;StreamSelector&lt;/span&gt; &lt;span class="pl-kos"&gt;}&lt;/span&gt; &lt;span class="pl-k"&gt;from&lt;/span&gt; &lt;span class="pl-s"&gt;"@sigyn/logql"&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;

&lt;span class="pl-k"&gt;const&lt;/span&gt; &lt;span class="pl-s1"&gt;api&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-k"&gt;new&lt;/span&gt; &lt;span class="pl-v"&gt;GrafanaApi&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-kos"&gt;{&lt;/span&gt;
  &lt;span class="pl-c1"&gt;authentication&lt;/span&gt;: &lt;span class="pl-kos"&gt;{&lt;/span&gt;
    &lt;span class="pl-c1"&gt;type&lt;/span&gt;: &lt;span class="pl-s"&gt;"bearer"&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
    &lt;span class="pl-c1"&gt;token&lt;/span&gt;: &lt;span class="pl-s1"&gt;process&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;env&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;GRAFANA_API_TOKEN&lt;/span&gt;&lt;span class="pl-c1"&gt;!&lt;/span&gt;
  &lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
  &lt;span class="pl-c1"&gt;remoteApiURL&lt;/span&gt;: &lt;span class="pl-s"&gt;"https://name.loki.com"&lt;/span&gt;
&lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;

&lt;span class="pl-k"&gt;const&lt;/span&gt; &lt;span class="pl-s1"&gt;ql&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-k"&gt;new&lt;/span&gt; &lt;span class="pl-v"&gt;LogQL&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;
  &lt;span class="pl-k"&gt;new&lt;/span&gt; &lt;span class="pl-v"&gt;StreamSelector&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-kos"&gt;{&lt;/span&gt; &lt;span class="pl-c1"&gt;app&lt;/span&gt;: &lt;span class="pl-s"&gt;"serviceName"&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-c1"&gt;env&lt;/span&gt;: &lt;span class="pl-s"&gt;"production"&lt;/span&gt; &lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;
&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
&lt;span class="pl-k"&gt;const&lt;/span&gt; &lt;span class="pl-s1"&gt;logs&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-k"&gt;await&lt;/span&gt; &lt;span class="pl-s1"&gt;api&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;Loki&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;queryRange&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;
  &lt;span class="pl-s1"&gt;ql&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-c"&gt;// or string `{app="serviceName", env="production"}`&lt;/span&gt;
  &lt;span class="pl-kos"&gt;{&lt;/span&gt;
    &lt;span class="pl-c1"&gt;start&lt;/span&gt;: &lt;span class="pl-s"&gt;"1d"&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
    &lt;span class="pl-c1"&gt;limit&lt;/span&gt;: &lt;span class="pl-c1"&gt;200&lt;/span&gt;
  &lt;span class="pl-kos"&gt;}&lt;/span&gt;
&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
&lt;span class="pl-smi"&gt;console&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;log&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;logs&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;…&lt;/p&gt;
&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/OpenAlly/loki" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;



&lt;h2&gt;
  
  
  🔎 LogQL builder
&lt;/h2&gt;

&lt;p&gt;We've also built a utility package to construct log queries, inspired by the WHATWG URL/URLSearchParams API.&lt;/p&gt;

&lt;p&gt;The package lets you build any LogQL programmatically from scratch&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;LogQL&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@sigyn/logql&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;logql&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;LogQL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;foo&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;logql&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;streamSelector&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;env&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;prod&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;logql&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lineEq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;bar&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;logql&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lineNotEq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;baz&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// {env=\"prod\"} |= `foo` |= `bar` != `baz`&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;logql&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But also able to parse raw string 😇&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;logql&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;LogQL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;{app=&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;foo&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;, env=&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;preprod&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;} |= `foo` != `bar`&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;([...&lt;/span&gt;&lt;span class="nx"&gt;logql&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;streamSelector&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;()]);&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;logql&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lineFilters&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lineContains&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;logql&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lineFilters&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lineDoesNotContain&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can use subclasses in your tools at any time, for example to retrieve labels from a LogQL 😮&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;StreamSelector&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@sigyn/logql&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;logql&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;...&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;labels&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;StreamSelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;logql&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;kv&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  📜 Credits
&lt;/h2&gt;

&lt;p&gt;I'm not alone and didn't do all the work 👯. Thanks to &lt;a href="https://github.com/PierreDemailly" rel="noopener noreferrer"&gt;Pierre Demailly&lt;/a&gt; and &lt;a href="https://github.com/SofianD" rel="noopener noreferrer"&gt;Sofian Doual&lt;/a&gt; for their contribution/support.&lt;/p&gt;

&lt;h2&gt;
  
  
  🙏 Conclusion
&lt;/h2&gt;

&lt;p&gt;These two tools have enabled us to initiate the development of new tools. One of them is an alerting agent for Loki named Sigyn, to whom I'll be dedicating an article in the near future.&lt;/p&gt;

&lt;p&gt;Having APIs really opens up a lot of opportunities and it'll be interesting to see what me and my team are able to produce in the future with them.&lt;/p&gt;

&lt;p&gt;Best Regards,&lt;br&gt;
Thomas&lt;/p&gt;

</description>
      <category>node</category>
      <category>javascript</category>
      <category>monitoring</category>
      <category>grafana</category>
    </item>
    <item>
      <title>Logs monitoring with Loki, Node.js and Fastify.js</title>
      <dc:creator>Thomas.G</dc:creator>
      <pubDate>Mon, 12 Jun 2023 12:45:10 +0000</pubDate>
      <link>https://dev.to/myunisoft/logs-monitoring-with-loki-nodejs-and-fastifyjs-3h8k</link>
      <guid>https://dev.to/myunisoft/logs-monitoring-with-loki-nodejs-and-fastifyjs-3h8k</guid>
      <description>&lt;p&gt;Hello 👋&lt;/p&gt;

&lt;p&gt;Over the past few months, I've been spending a lot of time creating dashboards on &lt;a href="https://grafana.com/" rel="noopener noreferrer"&gt;Grafana&lt;/a&gt; using &lt;a href="https://github.com/grafana/loki" rel="noopener noreferrer"&gt;Loki&lt;/a&gt; for &lt;a href="https://www.myunisoft.fr/" rel="noopener noreferrer"&gt;MyUnisoft&lt;/a&gt; (the company I work for).&lt;/p&gt;

&lt;p&gt;So I decided to write you an article to &lt;strong&gt;explain my adventure&lt;/strong&gt; from the point of view of a &lt;strong&gt;back-end Node.js&lt;/strong&gt; developer.&lt;/p&gt;

&lt;h2&gt;
  
  
  👀 Why grafana and loki?
&lt;/h2&gt;

&lt;p&gt;For context, the tool was implemented by a DevOps employee who left the company two years ago. So I didn't really choose the tool myself. I had no previous experience with either of these tools, so I had to experiment and discover as I went along.&lt;/p&gt;

&lt;p&gt;Grafana is pretty well-known and mature, so I wasn't worried. On the other side, I quickly liked Loki to manage and search my logs. So I thought, "&lt;strong&gt;I've got to learn to make dashboards out of this&lt;/strong&gt;".&lt;/p&gt;

&lt;h2&gt;
  
  
  📃 Writing good logs
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;💬 Several examples of logs are multiline to avoid scrolling and simplify reading.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;When you write logs, you must constantly think about the information they will provide and make sure they can be analyzed without too much difficulty 😵. It always takes several iterations to get a good result.&lt;/p&gt;

&lt;p&gt;In my experience, you can find two kinds of logs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;strong&gt;debug&lt;/strong&gt; logs you know are useless to your dashboards. They'll come in handy in case of bugs or when you're looking for more context about a given request.&lt;/li&gt;
&lt;li&gt;All other logs that can provide exploitable data for dashboards (and for a human reader)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here an example of a debug log we print when our services start (quite useful to customize the &lt;code&gt;--max-old-space-size&lt;/code&gt; flag).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;availableHeapSize&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;prettyBytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;v8&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getHeapStatistics&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;total_available_size&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Total available heap size: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;availableHeapSize&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Another common example is logging a JSON payload (you could exploit the data, of course, but that's often not the end goal).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;once&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;payloadReady&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;payload before publishing:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;😱 Be sure to redact the data (you may divulge confidential information without realizing it).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Most of the time, you won't get anything out of these logs, and they don't require any particular effort. In this article, I'm going to concentrate on the logs that we can exploit for our dashboards.&lt;/p&gt;

&lt;h3&gt;
  
  
  🐤 Practical example
&lt;/h3&gt;

&lt;p&gt;Here's an example of bad logs we initially set up for a document upload middleware/plugin&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Successfully uploaded '...'
//
Failed to upload '...'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There are several problems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No scope&lt;/strong&gt; (we can't easily search for all the logs related to our middleware).&lt;/li&gt;
&lt;li&gt;Can easily be &lt;strong&gt;mixed with other logs&lt;/strong&gt; (from other middlewares or services).&lt;/li&gt;
&lt;li&gt;It can be quite a challenge to &lt;strong&gt;parse the state&lt;/strong&gt; (failed or successful).&lt;/li&gt;
&lt;li&gt;Lack information about the &lt;strong&gt;request&lt;/strong&gt; or the &lt;strong&gt;user&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A better way of writing it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[uploader|req-x5d4] document 'name.jpg' uploaded (state: ok)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can easily extend the log with other information such as extension, size, runtime, etc. (without breaking the Loki pattern or regexp).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;(state: ok|ext: .jpg|size: 52.5 kB|upload-time: 0.503 ms)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;⚠️ Always use the same unit when logging size (bytes) or time (milliseconds). Setting the right 'unit' on Grafana will do the work of cleaning the value for you.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxz01vua03g7ry9lavzbc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxz01vua03g7ry9lavzbc.png" alt="grafana unit" width="685" height="624"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  📜 Format clarification
&lt;/h3&gt;

&lt;p&gt;A good log has to be structured to allow good searches with LogQL. Here's a bad example where it will be really difficult to extract information.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;hello-world.jpg 52.5 kB|0.503 ms
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I would recommend &lt;strong&gt;adding a label&lt;/strong&gt; before each value (it's also easier for a human to read). Having start, end, and separator &lt;strong&gt;characters&lt;/strong&gt; can also be a great help.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;doc 'hello-world.jpg' [size: 52.5 kB|exec: 0.503 ms]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here the required RegExp to parse all labels with Loki&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;doc '(?P&amp;lt;doc&amp;gt;\S+)' \[size: (?P&amp;lt;size&amp;gt;\S+) kB|exec: (?P&amp;lt;exec&amp;gt;\S+) ms\]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Loki 2.0 also allows you to retrieve labels with a simplified syntax called pattern.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pattern `doc '&amp;lt;doc&amp;gt;' [size: &amp;lt;size&amp;gt; kB|exec: &amp;lt;exec&amp;gt; ms]`
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  🔎 Implementation in the framework/code
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ code examples are simplified/truncated&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The Fastify framework includes the &lt;a href="https://github.com/pinojs/pino" rel="noopener noreferrer"&gt;Pino&lt;/a&gt; logger by default (a really great logger with lots of cool features that doesn't compromise on performance). The framework itself allows a lot of really cool stuff, like &lt;a href="https://www.nearform.com/blog/unlock-the-power-of-runtime-log-level-control/" rel="noopener noreferrer"&gt;controlling the level of logs at runtime&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;On my team, we have chosen to customize the default request and response logs to include additional information. To achieve this, you need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;setup fastify constructor option &lt;a href="https://www.fastify.io/docs/latest/Reference/Server/#disablerequestlogging" rel="noopener noreferrer"&gt;disableRequestLogging&lt;/a&gt; to &lt;code&gt;true&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;exploit two hooks (onRequest and onResponse).
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decorateRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;standardLog&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addHook&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;onRequest&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FastifyRequest&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;`(&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;) receiving request ...`&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;standardLog&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;standardLog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;bind&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addHook&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;onResponse&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FastifyRequest&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;standardLog&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="s2"&gt;`response returned "&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;method&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"
    )
  );
});
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The standardLog decorator allows us to display information about the request and the token in use (for authenticated endpoints).&lt;/p&gt;

&lt;p&gt;We have to handle several types of tokens (it all depends on who is consuming the API). A classical user or a partner through our partner API).&lt;/p&gt;

&lt;p&gt;Each of them includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The postgreSQL schema of the customer&lt;/li&gt;
&lt;li&gt;The user's or third-party's ID&lt;/li&gt;
&lt;li&gt;The accounting folder (which can be null)
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;standardLog&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FastifyRequest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hasRequestDecorator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tokenInfo&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tokenInfo&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;tokenInfo&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;tokenInfoLog&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;tokenInfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;switch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tokenInfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;api&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nx"&gt;tokenInfoLog&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="s2"&gt;`|s:&lt;/span&gt;&lt;span class="p"&gt;${...}&lt;/span&gt;&lt;span class="s2"&gt;|t:&lt;/span&gt;&lt;span class="p"&gt;${...}&lt;/span&gt;&lt;span class="s2"&gt;|acf:&lt;/span&gt;&lt;span class="p"&gt;${...}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nx"&gt;tokenInfoLog&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="s2"&gt;`|s:&lt;/span&gt;&lt;span class="p"&gt;${...}&lt;/span&gt;&lt;span class="s2"&gt;|p:&lt;/span&gt;&lt;span class="p"&gt;${...}&lt;/span&gt;&lt;span class="s2"&gt;|acf:&lt;/span&gt;&lt;span class="p"&gt;${...}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nl"&gt;default&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nx"&gt;tokenInfoLog&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`(&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;|&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;tokenInfoLog&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;) &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`(&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;|none) &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here's what it looks like in the logs&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;(req-1rti) receiving request "POST /ged/base-docs/docs"
(req-1rti|user|s:538|p:1|acf:23) response returned
"POST /ged/base-docs/docs", statusCode: 201 (460.106ms) 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These informations will be critical to populating our dashboards with information about customers and the scope of actions.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnvnm176pnjebx077yo8t.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnvnm176pnjebx077yo8t.png" alt="schema" width="767" height="379"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  🌀 Logs ingestion
&lt;/h2&gt;

&lt;p&gt;My team currently uses &lt;a href="https://grafana.com/docs/loki/latest/clients/promtail/" rel="noopener noreferrer"&gt;promtail&lt;/a&gt; with a YML configuration to retrieve service logs.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;server&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;http_listen_port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;9080&lt;/span&gt;
&lt;span class="na"&gt;positions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;filename&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/var/lib/promtail/positions.yml&lt;/span&gt;
&lt;span class="na"&gt;clients&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://xxx.fr/loki/api/v1/push&lt;/span&gt;

&lt;span class="na"&gt;scrape_configs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;job_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;svc_api_dev&lt;/span&gt;
    &lt;span class="na"&gt;static_configs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;__path__&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/home/xxx/logs/service-dev-*.log&lt;/span&gt;
        &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;api&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dev&lt;/span&gt;
        &lt;span class="na"&gt;host&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;xxx.fr&lt;/span&gt;
        &lt;span class="na"&gt;job&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;svc_api_dev&lt;/span&gt;
      &lt;span class="na"&gt;targets&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;localhost&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I'm not going to spend too much time on this chapter, as you can find a lot of tutorials on the Internet on how to set up and configure promtail.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 If you don't want to depend on promtail for sending the logs to Loki, you can use the fastify plugin &lt;a href="https://github.com/Julien-R44/pino-loki" rel="noopener noreferrer"&gt;pino-loki&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  📊 OCR Dashboard
&lt;/h2&gt;

&lt;p&gt;MyUnisoft is a French accounting editor, so &lt;strong&gt;O&lt;/strong&gt;ptical &lt;strong&gt;C&lt;/strong&gt;haracter &lt;strong&gt;R&lt;/strong&gt;ecognition is an important feature of our software. Monitoring is essential to adapting to the needs of our customers and responding promptly to incidents.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;👀😍 We won the &lt;a href="https://www.linkedin.com/posts/le-monde-du-chiffre_palmareslmc-palmareslmc2023-activity-7071905959412916225-pL4d?utm_source=share&amp;amp;utm_medium=member_desktop" rel="noopener noreferrer"&gt;OCR silver medal 2023&lt;/a&gt; awarded by Le Monde du Chiffre.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fk4fhao5sjfh337rulmbn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fk4fhao5sjfh337rulmbn.png" alt="myunisoft OCR" width="800" height="371"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here is an example of a dashboard we have built just by using the following log:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;(req-2987|user|s:24|p:5710|acf:7398) OCR xxx.jpg
[type: invoice|ext: .jpg|size: 2925.73 kB]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Group by label
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4srwlq3hjpu2n33dskfv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4srwlq3hjpu2n33dskfv.png" alt="extensions" width="662" height="311"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We can construct the above graph with the following logQL. Notice the &lt;strong&gt;sum&lt;/strong&gt; by &lt;strong&gt;extension&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight make"&gt;&lt;code&gt;&lt;span class="err"&gt;sum(&lt;/span&gt;
  &lt;span class="err"&gt;count_over_time(&lt;/span&gt;
    &lt;span class="nv"&gt;{app&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"ocr"&lt;/span&gt;,env&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$env&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt;
      &lt;span class="err"&gt;|=&lt;/span&gt; &lt;span class="s2"&gt;"OCR"&lt;/span&gt;
      &lt;span class="nl"&gt;|= "|ext&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="nf"&gt;"&lt;/span&gt;
      | regexp &lt;span class="sb"&gt;`&lt;/span&gt;&lt;span class="se"&gt;\|&lt;/span&gt;ext: &lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;?P&amp;lt;extension&amp;gt;&lt;span class="se"&gt;\S&lt;/span&gt;+&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="se"&gt;\|&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;
  &lt;span class="err"&gt;[$__range])&lt;/span&gt;
&lt;span class="err"&gt;)&lt;/span&gt; &lt;span class="err"&gt;by&lt;/span&gt; &lt;span class="err"&gt;(extension)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;RegExp are built using Golang syntax (I personnaly use &lt;a href="https://regex101.com/" rel="noopener noreferrer"&gt;regex101.com&lt;/a&gt; to test them). It is useful here to extract/detect the &lt;code&gt;extension&lt;/code&gt; label.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;We can use the syntax &lt;code&gt;$env&lt;/code&gt; to inject a dashboard variable (here I can view my dashboard in production, staging or dev just by moving a value in a select).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fef2kz5cgrus5r3bvsqfy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fef2kz5cgrus5r3bvsqfy.png" alt="grafana variable" width="233" height="114"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I also use the built-in &lt;code&gt;$__range&lt;/code&gt; variable, which loads data over the currently selected range in Grafana.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbdkn193fwnu5sl5spadn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbdkn193fwnu5sl5spadn.png" alt="grafana range" width="553" height="42"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Unwrap
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F40irbud9rf6hzc5wi3ew.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F40irbud9rf6hzc5wi3ew.png" alt="file size" width="675" height="323"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It took me a long time to really understand unwrap. There is virtually no clear documentation or explanation on the Internet! &lt;/p&gt;

&lt;p&gt;Here it will use the &lt;code&gt;size&lt;/code&gt; label and extract all values to compute them with &lt;code&gt;min_over_time&lt;/code&gt; and &lt;code&gt;min&lt;/code&gt; functions. This is useful for &lt;strong&gt;extracting numerical values&lt;/strong&gt; (counter, execution time, etc.).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight make"&gt;&lt;code&gt;&lt;span class="err"&gt;min(&lt;/span&gt;
  &lt;span class="err"&gt;min_over_time(&lt;/span&gt;
    &lt;span class="nv"&gt;{app&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"ocr"&lt;/span&gt;,env&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$env&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt;
      &lt;span class="err"&gt;|=&lt;/span&gt; &lt;span class="s2"&gt;"OCR"&lt;/span&gt;
      &lt;span class="nl"&gt;| regexp `\|size&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;(?P&amp;lt;size&amp;gt;[.0-9]+) kB&lt;/span&gt;\]&lt;span class="nf"&gt;`&lt;/span&gt;
      | unwrap size
      | __error__ &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;
  &lt;span class="err"&gt;[$__range])&lt;/span&gt;
&lt;span class="err"&gt;)&lt;/span&gt; &lt;span class="err"&gt;by&lt;/span&gt; &lt;span class="err"&gt;(app)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;💬 &lt;code&gt;| __error__ = ""&lt;/code&gt; avoid crashing Loki with unexpected Error (could happen if unwrap of size fail for any reasons).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Then, on the right side of the stat graph, we select the corresponding unit.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdeq12mj6xmkyqlrn0nhu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdeq12mj6xmkyqlrn0nhu.png" alt="grafana unit" width="435" height="111"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  💡 Tips and tricks
&lt;/h2&gt;

&lt;h3&gt;
  
  
  display name
&lt;/h3&gt;

&lt;p&gt;For a long time, I had some really bad raw labels in my charts (with a format similar to JSON).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgri579mofl2o4q38az7n.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgri579mofl2o4q38az7n.png" alt="grafana loki display name" width="478" height="81"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can customize it by editing the &lt;code&gt;display name&lt;/code&gt; option. You can use variables in this field to retrieve label values directly.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ff9rlnyc69zer7dkt87cd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ff9rlnyc69zer7dkt87cd.png" alt="grafana loki display name" width="411" height="91"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  no data
&lt;/h3&gt;

&lt;p&gt;Sometimes the graphs will display "no data" because no logs have been detected in the selected range. This can be problematic in certain graphics (such as gauges).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fiwcqoaixnt8ogh5yt08k.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fiwcqoaixnt8ogh5yt08k.png" alt="grafana no data" width="541" height="323"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;But maybe you'd rather have zero. No problem, just use the &lt;code&gt;No value&lt;/code&gt; option.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkjkl7oynwzdqjzcrv2rv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkjkl7oynwzdqjzcrv2rv.png" alt="grafana loki no data" width="409" height="91"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  filter using a regexp and variable
&lt;/h3&gt;

&lt;p&gt;On some dashboards, you will probably want to filter dynamically according to several criteria. One way of doing this is to use a regexp and a dashboard variable.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1fdxico6kvkrow78awkz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1fdxico6kvkrow78awkz.png" alt="grafana loki filter regexp" width="412" height="44"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is what I do in one of my dashboards to filter the results for one or more partners.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight make"&gt;&lt;code&gt;&lt;span class="err"&gt;count(&lt;/span&gt;
  &lt;span class="err"&gt;sum(&lt;/span&gt;
    &lt;span class="err"&gt;count_over_time(&lt;/span&gt;
      &lt;span class="nv"&gt;{app&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"api"&lt;/span&gt;,env&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"production"&lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt;
        &lt;span class="err"&gt;|=&lt;/span&gt; &lt;span class="s2"&gt;"] CALL"&lt;/span&gt;
        &lt;span class="err"&gt;|=&lt;/span&gt; &lt;span class="s2"&gt;"$endpoint"&lt;/span&gt;
        &lt;span class="err"&gt;|~&lt;/span&gt; &lt;span class="err"&gt;`\]\[$thirdparty\]`&lt;/span&gt;
        &lt;span class="nl"&gt;| regexp `\((?P&amp;lt;schemaId&amp;gt;[0-9]+)&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="nf"&gt;(?P&amp;lt;folderId&amp;gt;[0-9]+)&lt;/span&gt;\)&lt;span class="nf"&gt;`&lt;/span&gt;
    &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$__range&lt;/span&gt;&lt;span class="o"&gt;])&lt;/span&gt;
  &lt;span class="err"&gt;)&lt;/span&gt; &lt;span class="err"&gt;by&lt;/span&gt; &lt;span class="err"&gt;(schemaId,&lt;/span&gt; &lt;span class="err"&gt;folderId)&lt;/span&gt;
&lt;span class="err"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here's the line that does the job: you need to use backticks syntax to be able to inject a variable in it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;|~ `\]\[$thirdparty\]`
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  🚀 Data is the key to success
&lt;/h2&gt;

&lt;p&gt;As developers, we don't realize enough how monitoring and the data it generates can be powerful sources of improvement. At MyUnisoft, we work with over a &lt;strong&gt;hundred&lt;/strong&gt; partners. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkurcj1uwjk5c3qzv4fhk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkurcj1uwjk5c3qzv4fhk.png" alt="MyUnisoft Third-party" width="800" height="562"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Understanding how they use our APIs and the various anomalies they experience has become essential for us to continue to grow exponentially.&lt;/p&gt;

&lt;p&gt;This allows us to continually improve to provide a better experience for both our accounting customers and our developer partners who maintain the integrations.&lt;/p&gt;

&lt;p&gt;Our ability to materialize and exploit this data has become an essential part of my team's expertise. It's really exciting to see what can be achieved with simple logs.&lt;/p&gt;

&lt;h2&gt;
  
  
  👋 Conclusion
&lt;/h2&gt;

&lt;p&gt;I'm so glad I finally managed to write this article (and I hope it brought you some value). Many thanks to my team and especially to Cédric, without whom I would not have realized all that.&lt;/p&gt;

&lt;p&gt;I still struggle with a lot of Grafana features, such as transformations or alerts, but I will continue to dig and improve.&lt;/p&gt;

&lt;p&gt;🙏 Thanks for reading me 🙏&lt;/p&gt;

</description>
      <category>node</category>
      <category>javascript</category>
      <category>monitoring</category>
      <category>grafana</category>
    </item>
    <item>
      <title>Moving MyUnisoft Node.js back to TypeORM</title>
      <dc:creator>Thomas.G</dc:creator>
      <pubDate>Tue, 02 Aug 2022 18:53:46 +0000</pubDate>
      <link>https://dev.to/myunisoft/moving-myunisoft-nodejs-back-to-typeorm-3fok</link>
      <guid>https://dev.to/myunisoft/moving-myunisoft-nodejs-back-to-typeorm-3fok</guid>
      <description>&lt;p&gt;Hello 👋,&lt;/p&gt;

&lt;p&gt;Recently I took the time to reflect on my last two years at &lt;a href="https://www.myunisoft.fr/" rel="noopener noreferrer"&gt;MyUnisoft&lt;/a&gt;. I finally told myself that I wasn't writing enough about the difficulties we faced with my team 😊.&lt;/p&gt;

&lt;p&gt;Today I decided to write an article about our transition to &lt;a href="https://typeorm.io/" rel="noopener noreferrer"&gt;TypeORM&lt;/a&gt;. A choice we made over a year ago with my colleague &lt;a href="https://www.linkedin.com/in/alexandre-malaj-6062b0a6/" rel="noopener noreferrer"&gt;Alexandre MALAJ&lt;/a&gt; who joined a few months after me.&lt;/p&gt;

&lt;p&gt;We'll see why and how this choice allowed us to enhance the overall DX for my team 🚀. And that in the end it was a lot of trade-offs, and obviously, far from a perfect solution too.&lt;/p&gt;

&lt;h2&gt;
  
  
  🔍 The problem
&lt;/h2&gt;

&lt;p&gt;At MyUnisoft we work with a &lt;a href="https://www.postgresql.org/" rel="noopener noreferrer"&gt;PostgreSQL&lt;/a&gt; database with static and dynamic &lt;a href="https://www.postgresql.org/docs/current/ddl-schemas.html" rel="noopener noreferrer"&gt;schema&lt;/a&gt; (each client is isolated in one schema). And uniquely without counting the duplication of the schemas we have about 500 tables.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0alcskihv2wz9z2p09ms.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0alcskihv2wz9z2p09ms.png" alt="pgschemas" width="740" height="377"&gt;&lt;/a&gt; &lt;/p&gt;

&lt;p&gt;The Node.js stack was split into several services &lt;strong&gt;coupled to the database&lt;/strong&gt; (or to &lt;strong&gt;third-party&lt;/strong&gt; services for some of them). Developers before us were writing raw queries and there were no &lt;strong&gt;unit&lt;/strong&gt; or &lt;strong&gt;functional&lt;/strong&gt; tests 😬. When I took over as lead it was &lt;strong&gt;hell&lt;/strong&gt; to succeed in testing each service properly. Among the painful things 😱:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;strong coupling.&lt;/li&gt;
&lt;li&gt;heavy docker configuration&lt;/li&gt;
&lt;li&gt;complexity to generate business data for our tests.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We had to &lt;strong&gt;find a solution&lt;/strong&gt; to improve and secure our developments while &lt;strong&gt;iterating on production releases&lt;/strong&gt; 😵.&lt;/p&gt;

&lt;p&gt;Decentralizing with events wasn’t a possibility since of existing codes and dependencies (and we had no DevOps at the time).&lt;/p&gt;

&lt;h2&gt;
  
  
  💡 The solution
&lt;/h2&gt;

&lt;p&gt;We started thinking about &lt;strong&gt;creating an internal package&lt;/strong&gt; that would serve as an abstraction to interact with the database. We don't want to go for microservices 😉, so having a package that centralizes all this seems like a good compromise for us.&lt;/p&gt;

&lt;p&gt;Among our main objectives:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Generate a compliant database &lt;strong&gt;locally&lt;/strong&gt; or on &lt;strong&gt;Docker&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Easily generate fake data.&lt;/li&gt;
&lt;li&gt;Built to allow us to carry out our &lt;strong&gt;functional and business tests&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Centralized code review (which also allows us to track changes more easily)&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;We also had ideas like building a schema in a running database (which could be used for partner API testing and sandboxing).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The question remained whether we should &lt;strong&gt;continue writing raw queries&lt;/strong&gt; or not 😨. I'm not necessarily a big fan of ORMs, but we had a diversity of tables and requirements that made the writing of raw queries complicated at time.&lt;/p&gt;

&lt;p&gt;We looked at the different solutions in the ecosystem by checking our constraints with the schemas. After must research, we concluded that &lt;strong&gt;TypeORM was viable&lt;/strong&gt; (other ORM had critical issues).&lt;/p&gt;

&lt;p&gt;Far from perfect, but we had to &lt;strong&gt;give it a try&lt;/strong&gt; 💃!&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Note: with hindsight, we are now also very interested in &lt;a href="https://massivejs.org/" rel="noopener noreferrer"&gt;Massive.js&lt;/a&gt;. This could have been one of our choices.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  🐥 Let the story begin
&lt;/h2&gt;

&lt;h3&gt;
  
  
  👶 Baby steps
&lt;/h3&gt;

&lt;p&gt;My colleague Alexandre spent several months migrating the database on TypeORM 😮. I helped him by reviewing each table and relations.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;We did not opt for a migration script at the time (for us the choice was still too vague).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;We have made a gource to illustrate our work:&lt;br&gt;
&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/5rF6ogvUDiA"&gt;
&lt;/iframe&gt;
&lt;br&gt;
One of the problems we quickly encountered was that it was not possible to use the ActiveRecord pattern with dynamic schemas 😭. However this is ok for static schema because you can define them with the &lt;code&gt;@Entity&lt;/code&gt; decorator.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Entity&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sch_interglobal&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;JefactureWebhook&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;BaseEntity&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The management of datasources (connection) by schema/client was a bit infernal. We created our &lt;strong&gt;abstraction on top of TypeORM&lt;/strong&gt; to handle all this properly and regarding our schema initialization requirements.&lt;/p&gt;

&lt;p&gt;One of our encounters being quite complicated has been to &lt;strong&gt;clone a schema when we add a new client on the fly&lt;/strong&gt; 🐝(that's something we do in our tests, in the authentication service for example).&lt;/p&gt;

&lt;p&gt;We were able to achieve this by using the &lt;code&gt;@EventSubscriber&lt;/code&gt; decorator on a static table we use to register new customers’ information.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;EventSubscriber&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Sub_GroupeMembre&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;listenTo&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;Entities&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;schInterglobal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;GroupeMembre&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;afterInsert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;UpdateEvent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;idGroupeMembre&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;entity&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;queryManager&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;datasources&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;default&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;queryManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="s2"&gt;`SELECT clone_schema('sch1', 'sch&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;idGroupeMembre&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;')`&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;connection&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;DataSource&lt;/span&gt;&lt;span class="p"&gt;({})).&lt;/span&gt;&lt;span class="nf"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nx"&gt;datasources&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`sch&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;idGroupeMembre&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The tricky part was &lt;strong&gt;to build an SQL script to properly clone a schema&lt;/strong&gt; with all tables, relations, foreign keys etc.. But after many difficulties we still managed to get out of it 😅.&lt;/p&gt;

&lt;h3&gt;
  
  
  📜 Blueprints
&lt;/h3&gt;

&lt;p&gt;When I started this project I was inspired by &lt;a href="https://github.com/adonisjs/lucid" rel="noopener noreferrer"&gt;Lucid&lt;/a&gt; which is the ORM of the &lt;a href="https://adonisjs.com/" rel="noopener noreferrer"&gt;Adonis.js framework&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;By the way, Lucid was &lt;strong&gt;one of our choices&lt;/strong&gt;, but like many of &lt;a href="https://twitter.com/amanvirk1" rel="noopener noreferrer"&gt;Harminder&lt;/a&gt;'s packages, it is sometimes difficult to &lt;strong&gt;use them outside of Adonis&lt;/strong&gt; (which is not a criticism, it is sometimes understandable when the goal is to build a great DX for a framework).&lt;/p&gt;

&lt;p&gt;But I was quite a fan of Lucid's &lt;strong&gt;factory API&lt;/strong&gt; so we built &lt;strong&gt;an equivalent&lt;/strong&gt; with TypeORM that we called "Blueprint".&lt;/p&gt;

&lt;p&gt;Here is an example of a blueprint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;Blueprint&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;IConnectorLogs&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ConnectorLogsEntity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;faker&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;severity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;faker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;helpers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;arrayElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;values&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;connectorLogSeverities&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;faker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lorem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sentence&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;public&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;faker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;datatype&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="na"&gt;requestId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;faker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;datatype&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="na"&gt;readedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;createdAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;faker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;past&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="na"&gt;thirdPartyId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;faker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;datatype&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;number&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;min&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;max&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt; 
    &lt;span class="p"&gt;})),&lt;/span&gt;
    &lt;span class="na"&gt;idSociete&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The callback includes the faker lib as well as internal custom functions to generate accounting data. You can use this blueprint to generate data like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;Blueprints&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ConnectorLogs&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;merge&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;readedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The API is similar but it appears our objectives and TypeORM forced us to make different choices.&lt;/p&gt;

&lt;h4&gt;
  
  
  ES6 Proxy usage
&lt;/h4&gt;

&lt;p&gt;You may have noticed but something is weird with this API. Every time you hit &lt;code&gt;Blueprints.sch&lt;/code&gt; it triggers an &lt;strong&gt;ES6 proxy trap&lt;/strong&gt; that will return a new instance of a given Blueprint.&lt;/p&gt;

&lt;p&gt;It was quite satisfying for me to manage to use a Proxy for a real need and at the same time manage to return the right type with TypeScript.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;schBlueprints&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./sch/index&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Blueprint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;EntityBlueprint&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;../blueprint&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// CONSTANTS&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;kProxyHandler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;prop&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;prop&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;obj&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;prop&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;EmulateBlueprint&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nx"&gt;Blueprint&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;infer&lt;/span&gt; &lt;span class="nx"&gt;E&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;infer&lt;/span&gt; &lt;span class="nx"&gt;S&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt;
  &lt;span class="nx"&gt;EntityBlueprint&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;E&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;S&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;never&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;DeepEmulateBlueprint&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Blueprints&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="kr"&gt;keyof&lt;/span&gt; &lt;span class="nx"&gt;Blueprints&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt; &lt;span class="nx"&gt;EmulateBlueprint&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Blueprints&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Proxy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;schBlueprints&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;kProxyHandler&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;DeepEmulateBlueprint&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;schBlueprints&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  📟 Seeder
&lt;/h3&gt;

&lt;p&gt;We worked from the beginning of the project to build a relatively simple seeding API. The idea was mainly to be able to generate the static data required for our services to work properly.&lt;/p&gt;

&lt;p&gt;Here's an example of one simple seed script that generates static data with a blueprint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SeederRunOptions&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;seeder&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;seeder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sch_global.profil&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;sch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PersPhysique&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;doubleAuthRecoveryCodes&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createMany&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nx"&gt;seeder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;loadedTable&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;tableName&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When we generate a new database locally or in Docker we can see the execution of all the seeds:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Folg9hgfmyu82ovwv0tly.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Folg9hgfmyu82ovwv0tly.png" alt="seeder" width="800" height="458"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  🌀 Docker and testcontainers
&lt;/h3&gt;

&lt;p&gt;When &lt;a href="https://www.linkedin.com/in/tonygorez/" rel="noopener noreferrer"&gt;Tony Gorez&lt;/a&gt; was still working with us at MyUnisoft he was one of the first to work around how we can set up our tests inside a Docker and run them in our GitLab CI.&lt;/p&gt;

&lt;p&gt;The execution of our tests was relatively long (time to build the Docker etc). That's when he told us about something a friend had recommended to him: &lt;a href="https://github.com/testcontainers/testcontainers-node" rel="noopener noreferrer"&gt;testcontainers&lt;/a&gt; for Node.js.&lt;/p&gt;

&lt;p&gt;Once set up but what a magical feeling... The execution of our tests was faster by a ratio of 4x. Tony has been a great help and his work has &lt;strong&gt;allowed us to build the foundation&lt;/strong&gt; of the tests for our services.&lt;/p&gt;

&lt;p&gt;On my side I worked on an internal abstraction allowing everyone not to lose time on setup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;dotenv&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;config&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;testcontainers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@myunisoft/testcontainers&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kr"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exports&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;globalSetup&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;testcontainers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;containers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;postgres&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;redis&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
    &lt;span class="na"&gt;pgInitOptions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;seedsOptions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;tables&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
          &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sch_interglobal/groupeMembre&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sch_global/thirdPartyApiCategory&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;]&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  📦 Difficulties with a package 😱
&lt;/h3&gt;

&lt;p&gt;Not everything in the process goes smoothly 😕. In the beginning, it was really difficult to manage the versioning. We used to use npm link a lot to work with our local projects but it was far from perfect (it was more like hell 😈).&lt;/p&gt;

&lt;p&gt;And by the way, you have to be very careful with everything related to NPM &lt;strong&gt;peerDependencies&lt;/strong&gt; (especially with TypeScript). If you use a version of typeorm in the package, you necessarily must use the same one in the service otherwise you will have problems with types that do not match.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"peerDependencies"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"@myunisoft/postgre-installer"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^1.12.1"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We had the same issue with our internal Fastify plugin. It cost us a few days sometimes the time to understand that we had screwed up well on the subject 🙈.&lt;/p&gt;

&lt;p&gt;In the end, after some stabilizations, we could release new versions very quickly.&lt;/p&gt;

&lt;p&gt;I'm not necessarily completely satisfied with the DX on this subject at the moment and I'm thinking of improving it with automatic releases using our commits.&lt;/p&gt;

&lt;h3&gt;
  
  
  Others APIs
&lt;/h3&gt;

&lt;p&gt;I couldn't even cover everything because this project is so large. For example, we have a snapshot API that allows us to save and delete data during our tests...&lt;/p&gt;

&lt;p&gt;Speaking of tests, it is always difficult to give you examples without being boring. But there too the work was colossal.&lt;/p&gt;

&lt;p&gt;I would like to underline the work of &lt;a href="https://www.linkedin.com/in/cedric-lionnet-578845121/" rel="noopener noreferrer"&gt;Cédric Lionnet&lt;/a&gt; who has always been at the forefront when it came to solidifying our tests.&lt;/p&gt;

&lt;h2&gt;
  
  
  💸 Hard work pays off
&lt;/h2&gt;

&lt;p&gt;After one year of hard work the project is starting to be actively used by the whole team across all HTTP services 😍. Everyone starts to actively contribute (and a dozen developers on a project is a pretty interesting strike force ⚡).&lt;/p&gt;

&lt;p&gt;Sure we had a lot of &lt;strong&gt;issues&lt;/strong&gt; but we managed to solve them one by one 💪 (I'm not even talking about the migration to TypeORM 3.x 😭).&lt;/p&gt;

&lt;p&gt;But thanks to our effort, we are finally able to significantly improve the testing within our Node.js services. We can also start to work in localhost whereas before, developers used remote environments.&lt;/p&gt;

&lt;p&gt;In two years we have managed to recreate a healthy development environment with good practices and unit and functional testing on almost all our projects.&lt;/p&gt;

&lt;h2&gt;
  
  
  📢 My take on TypeORM
&lt;/h2&gt;

&lt;p&gt;If I were in the same situation tomorrow I would probably try another way/solution (like &lt;a href="https://massivejs.org/" rel="noopener noreferrer"&gt;Massive.js&lt;/a&gt;). For example, TypeORM poor performance will probably be a topic in the future for my team.&lt;/p&gt;

&lt;p&gt;As I said at the beginning, I'm not a fan of ORMs and in the context of personal projects, I do without them almost all the time.&lt;/p&gt;

&lt;p&gt;However, I must admit that we succeeded with TypeORM and that the result is not too bad either. There is probably no silver bullet 🤷.&lt;/p&gt;

&lt;h2&gt;
  
  
  🙇 Conclusion
&lt;/h2&gt;

&lt;p&gt;Many engineers would have given up at the beginning thinking that it would not be worth the energy to fight 😰.&lt;/p&gt;

&lt;p&gt;It's a bit simple to always want to start from scratch 😝. For me it was a challenge, to face reality which is sometimes hard to accept and forces us to make different choices 😉.&lt;/p&gt;

&lt;p&gt;It was also a great team effort with a lot of trusts 👯. We had invested a lot and as a lead I was afraid I had made the wrong choice. But with &lt;strong&gt;Alexandre&lt;/strong&gt; it is always a pleasure to see that today all this pays off.&lt;/p&gt;




&lt;p&gt;I'm not quoting everyone but thanks to those who actively helped and worked on the project especially in the early stage.&lt;/p&gt;

&lt;p&gt;Thanks for reading and as usual see you soon for a new article 😘&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>node</category>
      <category>typescript</category>
      <category>postgres</category>
    </item>
    <item>
      <title>MyUnisoft - l'aventure Node.js</title>
      <dc:creator>Thomas.G</dc:creator>
      <pubDate>Mon, 13 Sep 2021 06:46:31 +0000</pubDate>
      <link>https://dev.to/myunisoft/myunisoft-l-aventure-node-js-12i3</link>
      <guid>https://dev.to/myunisoft/myunisoft-l-aventure-node-js-12i3</guid>
      <description>&lt;p&gt;Bienvenue voyageur(se) 👋&lt;/p&gt;

&lt;p&gt;Aujourd'hui je viens vous conter mon aventure chez MyUnisoft en tant que lead technique back-end (API &amp;amp; &lt;a href="https://fr.wikipedia.org/wiki/Node.js" rel="noopener noreferrer"&gt;Node.js&lt;/a&gt;). C'est aussi celle de mon équipe qui continue de grandir en embarquant des ingénieurs très talentueux 😍.&lt;/p&gt;

&lt;p&gt;Si vous êtes un (expert-)comptable alors je vais vous embarquer dans un récit qui s'éloigne probablement de ce que vous avez l'habitude de lire 📰. Mais pas d'inquiétude je ferais l'effort de vous vulgariser au maximum mon univers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Qui suis-je ?
&lt;/h2&gt;

&lt;p&gt;Moi c'est Thomas, j'ai 27 ans et je développe depuis l'âge de dix ans 🐤. Je suis un amoureux du code et j'entreprends des projets depuis mon plus jeune âge.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fryq8wqfftzuotk2cq12n.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fryq8wqfftzuotk2cq12n.gif" alt="gif" width="498" height="278"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Je suis un expert Node.js et JavaScript. Fort aise sur des sujets comme la sécurité, le monitoring et l'architecture logicielle. Si mon parcours vous intéresse 👀 je vous invite à consulter mon &lt;a href="https://www.linkedin.com/in/thomas-gentilhomme/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Chapitre 1
&lt;/h2&gt;

&lt;p&gt;Découvrons sans attendre le premier chapitre 💃.&lt;/p&gt;

&lt;h3&gt;
  
  
  Genèse
&lt;/h3&gt;

&lt;p&gt;J'ai rejoint MyUnisoft en aout 2020 pour m'occuper de la maintenance et évolution du back-end Node.js 🐢. À ce moment-là je suis le seul développeur et ma première préoccupation est évidemment de faire mes preuves auprès de &lt;a href="https://www.linkedin.com/in/cyril-mandrilly/" rel="noopener noreferrer"&gt;Cyril&lt;/a&gt; (CTO) et &lt;a href="https://www.linkedin.com/in/r%C3%A9gis-samuel-3a910b18/" rel="noopener noreferrer"&gt;Régis&lt;/a&gt; (CEO). &lt;/p&gt;

&lt;p&gt;J'ai commencé par travailler sur la mise en place du connecteur &lt;a href="https://quickbooks.intuit.com/" rel="noopener noreferrer"&gt;Quickbooks&lt;/a&gt; pour ensuite très vite m'attaquer à l'évolution de l'&lt;a href="https://github.com/MyUnisoft/api-partenaires" rel="noopener noreferrer"&gt;API partenaires&lt;/a&gt; (qui servira aussi de fondation plus tard pour l'accès cabinet).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0bozkoxe62njsyo0lkqh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0bozkoxe62njsyo0lkqh.png" alt="image" width="800" height="404"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;L'écriture d'une documentation a été évidemment un des gros points pour garantir une meilleure expérience à nos partenaires (expérience que nous continuerons d'améliorer dans le temps).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzo2tlqe506ws96o33q3m.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzo2tlqe506ws96o33q3m.png" alt="image" width="710" height="490"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Ces premiers chantiers m'ont permis d'avoir une première approche du domaine de la comptabilité en abordant plusieurs notions comme les journaux, le plan comptable, les écritures, etc 😵.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Par ailleurs, je souhaite remercier Leon Souvannavong qui m'a beaucoup aidé sur les sujets métiers depuis mon intégration (ainsi que les autres développeurs de l'équipe back-end comptabilité 💖).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Novembre 2020
&lt;/h3&gt;

&lt;p&gt;Quelques mois passe et nous intégrons un second développeur en alternance 👯. Ayant déjà une forte expérience en mentorat je ne m'inquiète pas sur le fait de réussir à accompagner convenablement un débutant. Nous recrutons donc &lt;a href="https://www.linkedin.com/in/nicolas-hallaert/" rel="noopener noreferrer"&gt;Nicolas Hallaert&lt;/a&gt; qui ne cessera de me surprendre dans sa vitesse d'adaptation et d'apprentissage ⚡.&lt;/p&gt;

&lt;p&gt;Lui et moi avons travaillé ensemble sur divers sujets comme MyDataRH, le SSO, ou encore des interfaces génériques que vous retrouverez dans nos diverses interconnexions partenaires.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu7snluks4lhuk2bh74zr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu7snluks4lhuk2bh74zr.png" alt="image" width="800" height="400"&gt;&lt;/a&gt; &lt;/p&gt;

&lt;p&gt;Mon périmètre s'étend de plus en plus et je monte rapidement en confiance. Dans la même période &lt;a href="https://www.linkedin.com/in/oleh-sych-41245116a/" rel="noopener noreferrer"&gt;Oleh Sych&lt;/a&gt; rejoint l'équipe Node.js (développeur non francophone).&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;📌 Le seul développeur que je n'ai pas personnellement choisi. Au début j'avais un peu peur mais j'ai été très rapidement étonné par son niveau technique et sa réactivité à mes remarques.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Nous convenons très rapidement qu'il travaillera sur la mise à jour et migration de code "legacy" (écrit par des développeurs qui ne sont plus là). J'essaye de l'accompagner et de l'intégrer le mieux possible pour que la barrière de la langue ne soit pas un frein pour lui ✔️.&lt;/p&gt;

&lt;p&gt;En écrivant ces lignes aujourd'hui je peux témoigner du chemin parcouru avec lui. Nous allons de l'avant sur plusieurs projets (Gestion Électronique des Documents, Discussion, Crédit-bail entre autres).&lt;/p&gt;

&lt;h3&gt;
  
  
  Janvier 2021
&lt;/h3&gt;

&lt;p&gt;Après avoir démontré mes capacités et acquit la confiance de la direction &lt;strong&gt;je prends officiellement le lead de l'équipe Node.js&lt;/strong&gt; 🎉. C'est un rôle qui me convient bien et j'ai toujours apprécié ce genre de responsabilité.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4m0zhpc69b8sanxljc63.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4m0zhpc69b8sanxljc63.gif" alt="gif" width="250" height="236"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;J'interviens de plus en plus sur des sujets en lien avec l'authentification 🔑 et je prends rapidement la main dessus.&lt;/p&gt;

&lt;p&gt;Le reste de mon temps est dédié à la création d'un nouveau connecteur API avec &lt;a href="https://dext.com/fr" rel="noopener noreferrer"&gt;Dext&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Février 2021
&lt;/h3&gt;

&lt;p&gt;Une période chargée puisque nous avons embarqué deux nouveaux développeurs expérimentés dans l'équipe.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Le premier étant mon associé de longue date &lt;a href="https://www.linkedin.com/in/alexandre-malaj-6062b0a6/" rel="noopener noreferrer"&gt;Alexandre MALAJ&lt;/a&gt; avec qui je travaille en binôme depuis maintenant plus d'une décennie 😲.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Le second est &lt;a href="https://www.linkedin.com/in/cedric-lionnet-578845121/" rel="noopener noreferrer"&gt;Cédric LIONNET&lt;/a&gt; qui nous a été recommandé en interne. Il entame une transition vers Node.js après plusieurs années de C++. C'est un ingénieur rigoureux ainsi qu'un amoureux de la qualité de code 💎.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Ces deux intégrations ont été le point de départ de ce qui est aujourd'hui la fondation de l'équipe Node.js.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Alexandre&lt;/strong&gt; a investi des centaines d'heures sur la création d'une couche ORM (contenant +500 tables et +2,000 relations). &lt;strong&gt;Cédric&lt;/strong&gt; de son côté à grandement contribuer à l'ajout de tests unitaires et abstractions qui sont aujourd'hui activement utilisées au travers de nos services http.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwfhuxnuo01n55en30xu5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwfhuxnuo01n55en30xu5.png" alt="carbon (3)" width="800" height="585"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Fort de mon expérience de plus quatre ans en gestion d'équipe remote, nous travaillons rapidement à la mise en place de conventions et d'un modèle de communication efficace.&lt;/p&gt;

&lt;p&gt;Il est primordial de construire une bonne entente ainsi que diverses habitudes de communication orale pour pouvoir rapidement acquérir une symbiose des compétences techniques et humaines. &lt;/p&gt;

&lt;h3&gt;
  
  
  Mars 2021
&lt;/h3&gt;

&lt;p&gt;Je commence à travailler sur l'intégration d'un nouveau connecteur avec &lt;a href="https://www.emasphere.com/fr/" rel="noopener noreferrer"&gt;EmaSphere&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Quand Nicolas n'est pas en cours il travaille sur l'intégration SSO avec Zendesk (support) et 360 learning (MyAcademy). Sur le côté il travaille sur le Google sheet (les liens dynamiques).&lt;/p&gt;

&lt;p&gt;Avec Alexandre nous avons décidé de lancer une initiative DDD (&lt;a href="https://blog.octo.com/domain-driven-design-des-armes-pour-affronter-la-complexite/" rel="noopener noreferrer"&gt;Domain Driven Design&lt;/a&gt;) au sein de MyUnisoft.&lt;/p&gt;

&lt;p&gt;Amener de la qualité et de la rigueur dans les échanges et la conception du logiciel est pour moi très important. Insuffler une meilleure compréhension du métier aux équipes techniques apporteraient énormément de valeurs à nos clients.&lt;/p&gt;

&lt;h3&gt;
  
  
  Avril 2021
&lt;/h3&gt;

&lt;p&gt;J'accompagne très activement de plus en plus de partenaires 😎. Le catalogue des connecteurs ne cessent de grandir ce qui me fait vraiment plaisir 😇.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftv41itzdhq6v2cvq6r1r.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftv41itzdhq6v2cvq6r1r.png" alt="image" width="800" height="449"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Et &lt;strong&gt;encore beaucoup d'autres&lt;/strong&gt; intégrations sont à venir d'ici fin 2021. Nous travaillons en ce moment même sur une mise à jour conséquente qui aura pour objectif d'apporter un ensemble de fonctionnalités manquantes (paramétrages, logs ...).&lt;/p&gt;




&lt;p&gt;Avec l'équipe nous participons à la &lt;a href="https://ldjam.com/" rel="noopener noreferrer"&gt;ludum dare&lt;/a&gt; 48 qui consiste à créer un jeu vidéo en 72h. Nous avons créé un jeu web utilisant le moteur Pixi.js (&lt;a href="https://github.com/fraxken/yu-gi" rel="noopener noreferrer"&gt;projet ici&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Une expérience très enrichissante qui nous aura permis de mieux nous connaître et de renforcer nos liens.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mai 2021
&lt;/h3&gt;

&lt;p&gt;L'équipe intègre deux développeurs supplémentaires:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://www.linkedin.com/in/karasu-tan-12641447/" rel="noopener noreferrer"&gt;Tan Karasu&lt;/a&gt; qui nous rejoint pour un stage de six mois. Développeur en reconversion qui a su me convaincre par son mental et son investissement.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://www.linkedin.com/in/mark-malaj-99b1b8b7/" rel="noopener noreferrer"&gt;Mark Malaj&lt;/a&gt; cousin d'Alexandre. Nous avions déjà eu l'occasion de collaborer ensemble pendant une année, période pendant laquelle je l'ai formé à Node.js. C'est naturellement un plaisir pour moi de pouvoir recollaborer avec lui au sein de MyUnisoft.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Alexandre et Mark travailleront en collaboration avec &lt;a href="https://www.linkedin.com/in/jeanclaudefortier/" rel="noopener noreferrer"&gt;Jean-Claude FORTIER&lt;/a&gt; sur la conception et le développement de la Gestion Interne MyUnisoft. Un chantier qui est donc entre de bonnes mains.&lt;/p&gt;

&lt;p&gt;Tan de son côté aura investi énormément de temps sur la création de nouvelle abstractions pour communiquer avec notre base de données &lt;a href="https://redis.io/" rel="noopener noreferrer"&gt;Redis&lt;/a&gt;. Par ailleurs, nos projets utiliseront l'excellent package &lt;a href="https://github.com/luin/ioredis#readme" rel="noopener noreferrer"&gt;ioredis&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Juin 2021
&lt;/h3&gt;

&lt;p&gt;J'ai eu l'occasion de travailler sur l'implémentation et l'intégration du format &lt;a href="https://fnfe-mpe.org/factur-x/" rel="noopener noreferrer"&gt;Factur-X&lt;/a&gt; pour nos partenaires (actuellement utilisé en production par &lt;a href="https://www.ebp.com/" rel="noopener noreferrer"&gt;EBP&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Une bonne occasion de jouer avec les nouveaux types de TypeScript 4 pour convertir dynamiquement les structures XML en type JSON propre.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvq6flhw4u6mhufn3beph.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvq6flhw4u6mhufn3beph.png" alt="carbon (1)" width="800" height="413"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;J'éprouve une certaine fatigue à cause des différents onboardings. C'est une première pour moi de gérer autant d'intégration en si peu de temps (même si cela reste une excellente expérience).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1irbvbj7doga59iq85i5.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1irbvbj7doga59iq85i5.gif" alt="gif" width="480" height="386"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Il est parfois difficile de jongler entre ma vélocité personnelle qui me permet d'avancer des sujets métier critique et investir du temps en accompagnement de mon équipe (ce qui améliore probablement la vélocité à moyen-long terme).&lt;/p&gt;

&lt;h3&gt;
  
  
  Aout 2021
&lt;/h3&gt;

&lt;p&gt;L'équipe continue de se structurer 🔨 dans le bon sens et nous avançons positivement sur nos sujets. La période est relativement calme à cause des différents départs en vacances 🌞.&lt;/p&gt;

&lt;p&gt;Nous intégrons néanmoins encore deux développeurs expérimentés:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://www.linkedin.com/in/quentin-lepateley/" rel="noopener noreferrer"&gt;Quentin Lepateley&lt;/a&gt; travaillant sur le frontend MyUnisoft depuis un an et demi. Ce n'est donc pas un petit nouveau et il arrive dans l'équipe en étant déjà familier avec les membres de l'équipe.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://www.linkedin.com/in/tonygorez/" rel="noopener noreferrer"&gt;Tony Gorez&lt;/a&gt; nous venant tout droit de &lt;a href="https://payfit.com/fr/" rel="noopener noreferrer"&gt;Payfit&lt;/a&gt;. Je travaille depuis maintenant une bonne année avec lui sur des projets open source comme &lt;a href="https://github.com/NodeSecure" rel="noopener noreferrer"&gt;NodeSecure&lt;/a&gt;. C'est vraiment un grand plaisir de pouvoir travailler avec lui au sein de la même équipe!&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Quentin travaille activement sur notre migration vers le framework &lt;a href="https://www.fastify.io/" rel="noopener noreferrer"&gt;Fastify.js&lt;/a&gt;. L'idée est de rapidement mettre en place un monorepo utilisant la fonctionnalité de &lt;a href="https://docs.npmjs.com/cli/v7/using-npm/workspaces" rel="noopener noreferrer"&gt;workspace npm 7&lt;/a&gt; pour héberger les différents plugins utilisés sur nos services.&lt;/p&gt;

&lt;p&gt;Tony quant à lui va rapidement venir m'épauler sur les intégrations partenaires. À court terme il travaillera sur la stabilisation du connecteur Quickbooks.&lt;/p&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/4DxyjZH45Jk"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  Mon sentiment sur l'équipe
&lt;/h2&gt;

&lt;p&gt;Il reste du chemin à parcourir c'est une certitude. Nous devons apprendre à mieux nous connaître et comprendre qu'elles sont les forces et faiblesses de chacun.&lt;/p&gt;

&lt;p&gt;Nous devons définir qu'elles seront nos pratiques et méthodologies tout en prenant évidemment en compte le contexte et les équipes qui nous entourent.&lt;/p&gt;

&lt;p&gt;Mais je suis très enthousiaste. Nous avons beaucoup d'appétence pour notre métier et une grande motivation à faire devenir réalité les ambitions de MyUnisoft.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmoyfq2titfci3aluhsrz.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmoyfq2titfci3aluhsrz.gif" alt="image" width="600" height="337"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  En avant pour un second chapitre ?
&lt;/h2&gt;

&lt;p&gt;Nous continuons de grandir et nombreux sont les challenges devant nous. &lt;strong&gt;De belle intégration sont encore à venir&lt;/strong&gt; et je pense que MyUnisoft constitue l'une des meilleures équipes Node.js francophone 💪.&lt;/p&gt;

&lt;p&gt;C'est pour moi une fierté d'être à la tête d'un groupe d'ingénieurs que j'apprécie et respecte 🙇. J'ai vraiment hâte de voir ce que nous allons accomplir dans les prochains mois 🚀.&lt;/p&gt;




&lt;p&gt;🙏 Merci à vous de m'avoir lu. &lt;/p&gt;

&lt;p&gt;Cet article a été volontairement épuré de beaucoup de détails techniques (mais j'espère tout de même avoir réussi à accrocher un peu de votre attention).&lt;/p&gt;

&lt;p&gt;Nous écrirons certainement plus d'articles à l'avenir pour vous parler de nos innovations et avancement technique.&lt;/p&gt;

&lt;p&gt;🚀🚀🚀&lt;/p&gt;

</description>
      <category>myunisoft</category>
      <category>node</category>
      <category>javascript</category>
    </item>
  </channel>
</rss>
