<?xml version="1.0" encoding="UTF-8"?><?xml-stylesheet href="/rss/styles.xsl" type="text/xsl"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Devin Riegle&apos;s Blog</title><description>Devin Riegle&apos;s personal blog to write about his experience in software engineering, fintech, business and startups.</description><link>https://www.riegle.dev/</link><language>en-us</language><item><title>Pilot</title><link>https://www.riegle.dev/blogs/pilot/</link><guid isPermaLink="true">https://www.riegle.dev/blogs/pilot/</guid><pubDate>Thu, 31 Dec 2020 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Hi there.&lt;/h2&gt;
&lt;p&gt;My name is Devin. Welcome to my blog. I&apos;m just getting it all setup and tweaking the right way. I&apos;m using Gatsby and Netlify and it has been really fun getting setup.&lt;/p&gt;
&lt;p&gt;I&apos;m excited to start writing on a regular schedule now that this is setup. I&apos;m planning to post once a week. I have a lot of ideas tech and finance related that I&apos;m excited to share. I&apos;ll also be sharing more about my journey into tech and the tools and resources I used to learn to code.&lt;/p&gt;
&lt;p&gt;If you&apos;d like to learn more about me now to decide if you&apos;re interested, check out my &lt;a href=&quot;/about/&quot;&gt;about me page&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;2021 Goals&lt;/h3&gt;
&lt;p&gt;It&apos;s New Year&apos;s Eve and I&apos;m tired of 2020 and I&apos;m &lt;strong&gt;SO&lt;/strong&gt; glad to reach the last day of this year, so let&apos;s set some goals.&lt;/p&gt;
&lt;p&gt;I&apos;m trying to set realistic goals this year. I had some super ambitious goals for 2020 and it was hard to reach them. They were the kind of goals where you felt like you failed if you didn&apos;t do them from Day 1. I&apos;d rather set goals that I could reset each month/quarter instead.&lt;/p&gt;
&lt;p&gt;If I fail to read 1 book in January, then on February 1st, I can start again. If I do it 11 months out of 12, I&apos;ll still feel successful.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Read at least 1 book per month&lt;/li&gt;
&lt;li&gt;Write 1 blog post per week&lt;/li&gt;
&lt;li&gt;Take more vacation time
&lt;ul&gt;
&lt;li&gt;I did not take enough time off in 2020 and I&apos;m feeling it.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Travel more&lt;/li&gt;
&lt;li&gt;Upload 1 Youtube video per quarter.
&lt;ul&gt;
&lt;li&gt;I know this should be more if I want to build my channel, but I only have a few videos on there now from late 2019 and I still get subscribers and views from it. I&apos;m at around 8.3k total views now. Hopefully I can grow that in 2021.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Happy New Year to you and your family. Let&apos;s all wish that 2021 will be a better year than 2020.&lt;/p&gt;
</content:encoded></item><item><title>Use Netlify Functions to upload files to Google Drive</title><link>https://www.riegle.dev/blogs/netlify-functions-to-upload-to-google-drive/</link><guid isPermaLink="true">https://www.riegle.dev/blogs/netlify-functions-to-upload-to-google-drive/</guid><pubDate>Sat, 05 Jun 2021 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;I recently worked on a project where the client wanted to upload images to Google Drive from their Wordpress site. They wanted a custom built
image uploader that could allow the users to drag and drop their photos to order them and then upload to a Google Drive. I&apos;m going to talk about how I used
Netlify Functions to solve this problem and how you can use Netlify Functions for almost any type of backend logic that you need.&lt;/p&gt;
&lt;p&gt;I&apos;ve had great success working with Netlify both for hosting frontend projects and for hosting Lambdas with their Functions product.
The best part about Netlify is that almost every time I work with it, it &lt;em&gt;just works&lt;/em&gt;. There are very few CI/CD tools that provide that
same reliability.&lt;/p&gt;
&lt;p&gt;Add the busboy package:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;yarn add busboy
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Let&apos;s start with a basic handler and the structure that we want:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export const handler = async (event) =&amp;gt; {
  try {
    if (!event.body || !event.isBase64Encoded) {
      return {
        statusCode: 400,
        body: JSON.stringify({
          message: &quot;no data&quot;,
        }),
      };
    }

    // Attempt to process file and fields sent up in the request using busboy

    // Upload to Google Drive

    // return the file ID and URL for viewing on the client

    return {
      statusCode: 200,
      body: JSON.stringify({
        fileId: &apos;&apos;
        fileUrl: &apos;&apos;,
      }),
    };
  } catch (error) {
    return {
      statusCode: 400,
      body: JSON.stringify({ error }),
    };
  }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now let&apos;s add our code to pull the image data and any fields out from the event:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export const processImageUpload = async (event) =&amp;gt; {
  return new Promise((resolve, reject) =&amp;gt; {
    const busboy = new Busboy({
      headers: {
        ...event.headers,
        &quot;content-type&quot;: event.headers[&quot;Content-Type&quot;] ?? event.headers[&quot;content-type&quot;],
      },
    });

    const result = {
      fields: {},
      files: [],
    };

    busboy.on(&quot;file&quot;, (_fieldname, file, fileName, encoding, contentType) =&amp;gt; {
      console.log(`Processed file ${fileName}`);

      file.on(&quot;data&quot;, (data) =&amp;gt; {
        result.files.push({
          file: data,
          fileName,
          encoding,
          contentType,
        });
      });
    });

    busboy.on(&quot;field&quot;, (fieldName, value) =&amp;gt; {
      console.log(`Processed field ${fieldName}: ${value}`);
      result.fields[fieldName] = value;
    });

    busboy.on(&quot;finish&quot;, () =&amp;gt; resolve(result));
    busboy.on(&quot;error&quot;, (error) =&amp;gt; reject(`Parse error: ${error}`));

    // pushes the event data into busboy to start the processing and using the event.isBase64Encoded property to tell which kind of data
    // we&apos;re working with
    busboy.write(event.body, event.isBase64Encoded ? &quot;base64&quot; : &quot;binary&quot;);

    busboy.end();
  });
};
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;import { processImageUpload } from &apos;./helpers/processImageUpload&apos;;

export const handler = async (event) =&amp;gt; {
  try {
    if (!event.body || !event.isBase64Encoded) {
      return {
        statusCode: 400,
        body: JSON.stringify({
          message: &quot;no data&quot;,
        }),
      };
    }

    // Attempt to process file and fields sent up in the request using busboy
   const { files, fields } = await processImageUpload(event);

    // Upload to Google Drive

    // return the file ID and URL for viewing on the client

    return {
      statusCode: 200,
      body: JSON.stringify({
        fileId: &apos;&apos;
        fileUrl: &apos;&apos;,
      }),
    };
  } catch (error) {
    return {
      statusCode: 400,
      body: JSON.stringify({ error }),
    };
  }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now we need to build the code to upload to Google Drive. You&apos;ll want to make sure you&apos;ve created a new project in the Google Cloud Console and have credentials that allow you Google Drive access.
After downloading the credentials file from Google, you can set all these values as environment variables in Netlify for your project.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function getCredentials() {
  const credentials = {
    type: &quot;service_account&quot;,
    project_id: process.env.GOOGLE_SERVICE_ACCOUNT_PROJECT_ID,
    private_key_id: process.env.GOOGLE_SERVICE_ACCOUNT_PRIVATE_KEY_ID,
    private_key: process.env.GOOGLE_SERVICE_ACCOUNT_PRIVATE_KEY.replace(/\\n/gm, &quot;\n&quot;),
    client_email: process.env.GOOGLE_SERVICE_ACCOUNT_CLIENT_EMAIL,
    client_id: process.env.GOOGLE_SERVICE_ACCOUNT_CLIENT_ID,
    auth_uri: &quot;https://accounts.google.com/o/oauth2/auth&quot;,
    token_uri: &quot;https://oauth2.googleapis.com/token&quot;,
    auth_provider_x509_cert_url: &quot;https://www.googleapis.com/oauth2/v1/certs&quot;,
    client_x509_cert_url: process.env.GOOGLE_SERVICE_ACCOUNT_CLIENT_CERT_URL,
  };

  let errorMessage = &quot;&quot;;

  for (const key of Object.keys(credentials)) {
    if (!credentials[key]) {
      errorMessage += `${key} must be defined, but was not.`;
    }
  }

  if (errorMessage.length) {
    throw new Error(errorMessage);
  }

  return credentials;
}

const getDrive = () =&amp;gt; {
  const auth = new google.auth.GoogleAuth({
    credentials: getCredentials(),
    scopes: [&quot;https://www.googleapis.com/auth/drive&quot;],
  });

  return google.drive({
    auth,
    version: &quot;v3&quot;,
  });
};

export const uploadFile = async ({ name, parents, fileContent, mimeType, originalFileName }) =&amp;gt; {
  const drive = getDrive();

  // upload file
  const file = await drive.files.create({
    requestBody: {
      name,
      mimeType,
      parents,
    },
    media: {
      mimeType,
      body: Buffer.isBuffer(fileContent) ? Readable.from(fileContent) : fileContent,
    },
  });

  // set permissions
  await drive.permissions.create({
    fileId: file.data.id,
    requestBody: {
      type: &quot;anyone&quot;,
      role: &quot;reader&quot;,
    },
  });

  return file;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;import { processImageUpload } from &quot;./helpers/processImageUpload&quot;;
import { uploadImage } from &quot;./helpers/googleDrive&quot;;

export const handler = async (event) =&amp;gt; {
  try {
    if (!event.body || !event.isBase64Encoded) {
      return {
        statusCode: 400,
        body: JSON.stringify({
          message: &quot;no data&quot;,
        }),
      };
    }

    // Attempt to process file and fields sent up in the request using busboy
    const { files, fields } = await processImageUpload(event);

    // Upload to Google Drive
    const file = files[0];

    if (!file) {
      return {
        statusCode: 400,
        body: JSON.stringify({
          message: &quot;no file uploaded&quot;,
        }),
      };
    }

    const uploadedFile = await uploadFile(file.fileName, {
      fileContent: file.file,
      mimeType: file.contentType,
      originalFileName: file.fileName,
      parents: [fields.folderId],
    });

    if (uploadedFile.status !== 200) {
      return {
        statusCode: uploadedFile.status,
        body: JSON.stringify({
          statusText: uploadedFile.statusText,
        }),
      };
    }

    // return the file ID and URL for viewing on the client

    return {
      statusCode: 200,
      body: JSON.stringify({
        fileId: uploadedFile.id,
        fileUrl: &quot;&quot;,
      }),
    };
  } catch (error) {
    return {
      statusCode: 400,
      body: JSON.stringify({ error }),
    };
  }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Let&apos;s add a quick little function to our &lt;code&gt;googleDrive.js&lt;/code&gt; file to get the public URL so the client can view the file.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export const getPublicUrl = async (fileId) =&amp;gt; {
  const drive = getDrive();

  const file = await drive.files.get({
    fileId,
    fields: &quot;id,webContentLink&quot;,
  });

  return file.data.webContentLink;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;import { processImageUpload } from &quot;./helpers/processImageUpload&quot;;
import { uploadImage, getPublicUrl } from &quot;./helpers/googleDrive&quot;;

export const handler = async (event) =&amp;gt; {
  try {
    if (!event.body || !event.isBase64Encoded) {
      return {
        statusCode: 400,
        body: JSON.stringify({
          message: &quot;no data&quot;,
        }),
      };
    }

    // Attempt to process file and fields sent up in the request using busboy
    const { files, fields } = await processImageUpload(event);

    // Upload to Google Drive
    const file = files[0];

    if (!file) {
      return {
        statusCode: 400,
        body: JSON.stringify({
          message: &quot;no file uploaded&quot;,
        }),
      };
    }

    const uploadedFile = await uploadFile(file.fileName, {
      fileContent: file.file,
      mimeType: file.contentType,
      originalFileName: file.fileName,
      parents: [fields.folderId],
    });

    if (uploadedFile.status !== 200) {
      return {
        statusCode: uploadedFile.status,
        body: JSON.stringify({
          statusText: uploadedFile.statusText,
        }),
      };
    }

    // return the file ID and URL for viewing on the client
    const fileUrl = await getPublicUrl(uploadedFile.id);

    return {
      statusCode: 200,
      body: JSON.stringify({
        fileId: uploadedFile.id,
        fileUrl,
      }),
    };
  } catch (error) {
    return {
      statusCode: 400,
      body: JSON.stringify({ error }),
    };
  }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That concludes the code needed to upload one file to GoogleDrive using Netlify Functions. Let me know if you have any questions. The full code is below if you want to copy and paste to use in your own projects.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export const processImageUpload = async (event) =&amp;gt; {
  return new Promise((resolve, reject) =&amp;gt; {
    const busboy = new Busboy({
      headers: {
        ...event.headers,
        &quot;content-type&quot;: event.headers[&quot;Content-Type&quot;] ?? event.headers[&quot;content-type&quot;],
      },
    });

    const result: Result = {
      fields: {},
      files: [],
    };

    busboy.on(&quot;file&quot;, (_fieldname, file, fileName, encoding, contentType) =&amp;gt; {
      console.log(`Processed file ${fileName}`);

      file.on(&quot;data&quot;, (data) =&amp;gt; {
        result.files.push({
          file: data,
          fileName,
          encoding,
          contentType,
        });
      });
    });

    busboy.on(&quot;field&quot;, (fieldName, value) =&amp;gt; {
      console.log(`Processed field ${fieldName}: ${value}`);
      result.fields[fieldName] = value;
    });

    busboy.on(&quot;finish&quot;, () =&amp;gt; resolve(result));
    busboy.on(&quot;error&quot;, (error) =&amp;gt; reject(`Parse error: ${error}`));

    busboy.write(event.body, event.isBase64Encoded ? &quot;base64&quot; : &quot;binary&quot;);

    busboy.end();
  });
};
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;function getCredentials(): Credentials {
  const credentials = {
    type: &quot;service_account&quot;,
    project_id: process.env.GOOGLE_SERVICE_ACCOUNT_PROJECT_ID,
    private_key_id: process.env.GOOGLE_SERVICE_ACCOUNT_PRIVATE_KEY_ID,
    private_key: process.env.GOOGLE_SERVICE_ACCOUNT_PRIVATE_KEY.replace(/\\n/gm, &quot;\n&quot;),
    client_email: process.env.GOOGLE_SERVICE_ACCOUNT_CLIENT_EMAIL,
    client_id: process.env.GOOGLE_SERVICE_ACCOUNT_CLIENT_ID,
    auth_uri: &quot;https://accounts.google.com/o/oauth2/auth&quot;,
    token_uri: &quot;https://oauth2.googleapis.com/token&quot;,
    auth_provider_x509_cert_url: &quot;https://www.googleapis.com/oauth2/v1/certs&quot;,
    client_x509_cert_url: process.env.GOOGLE_SERVICE_ACCOUNT_CLIENT_CERT_URL,
  };

  let errorMessage = &quot;&quot;;

  for (const key of Object.keys(credentials)) {
    if (!credentials[key]) {
      errorMessage += `${key} must be defined, but was not.`;
    }
  }

  if (errorMessage.length) {
    throw new Error(errorMessage);
  }

  return credentials;
}

const getDrive = () =&amp;gt; {
  const auth = new google.auth.GoogleAuth({
    credentials: getCredentials(),
    scopes: [&quot;https://www.googleapis.com/auth/drive&quot;],
  });

  return google.drive({
    auth,
    version: &quot;v3&quot;,
  });
};

export const uploadFile = async (
  name: string,
  {
    parents,
    fileContent,
    mimeType,
    originalFileName,
  }: {
    parents: string[],
    fileContent: Buffer | string,
    mimeType: string,
    originalFileName: string,
  }
) =&amp;gt; {
  const drive = getDrive();

  // upload file
  const file = await drive.files.create({
    requestBody: {
      name,
      mimeType,
      parents,
    },
    media: {
      mimeType,
      body: Buffer.isBuffer(fileContent) ? Readable.from(fileContent) : fileContent,
    },
  });

  // set permissions
  await drive.permissions.create({
    fileId: file.data.id,
    requestBody: {
      type: &quot;anyone&quot;,
      role: &quot;reader&quot;,
    },
  });

  return file;
};

export const getPublicUrl = async (fileId) =&amp;gt; {
  const drive = getDrive();

  const file = await drive.files.get({
    fileId,
    fields: &quot;id,webContentLink&quot;,
  });

  return file.data.webContentLink;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;import { processImageUpload } from &quot;./helpers/processImageUpload&quot;;
import { uploadImage, getPublicUrl } from &quot;./helpers/googleDrive&quot;;

export const handler = async (event) =&amp;gt; {
  try {
    if (!event.body || !event.isBase64Encoded) {
      return {
        statusCode: 400,
        body: JSON.stringify({
          message: &quot;no data&quot;,
        }),
      };
    }

    // Attempt to process file and fields sent up in the request using busboy
    const { files, fields } = await processImageUpload(event);

    // Upload to Google Drive
    const file = files[0];

    if (!file) {
      return {
        statusCode: 400,
        body: JSON.stringify({
          message: &quot;no file uploaded&quot;,
        }),
      };
    }

    const uploadedFile = await uploadFile(file.fileName, {
      fileContent: file.file,
      mimeType: file.contentType,
      originalFileName: file.fileName,
      parents: [fields.folderId],
    });

    if (uploadedFile.status !== 200) {
      return {
        statusCode: uploadedFile.status,
        body: JSON.stringify({
          statusText: uploadedFile.statusText,
        }),
      };
    }

    // return the file ID and URL for viewing on the client
    const fileUrl = await getPublicUrl(uploadedFile.id);

    return {
      statusCode: 200,
      body: JSON.stringify({
        fileId: uploadedFile.id,
        fileUrl,
      }),
    };
  } catch (error) {
    return {
      statusCode: 400,
      body: JSON.stringify({ error }),
    };
  }
};
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Say Goodbye to Gatsby and Hello to Remix</title><link>https://www.riegle.dev/blogs/remix-blog-rewrite/</link><guid isPermaLink="true">https://www.riegle.dev/blogs/remix-blog-rewrite/</guid><pubDate>Sat, 01 Jan 2022 00:00:00 GMT</pubDate><content:encoded>&lt;h3&gt;New Year. New Me. New &lt;em&gt;Blog&lt;/em&gt;.&lt;/h3&gt;
&lt;p&gt;I rewrote my blog to use &lt;a href=&quot;https://remix.run&quot;&gt;Remix&lt;/a&gt;. The latest hottest new full stack framework to hit Javascript. My previous version of the blog was using &lt;a href=&quot;https://gatsbyjs.com&quot;&gt;Gatsby.js&lt;/a&gt;. I decided to try this new framework out and try to rewrite my blog to see how it easy it was.&lt;/p&gt;
&lt;p&gt;My original version of the blog was created on January 1st, 2021 and, as many Gatsby sites do, I had a lot of plugins. Plugins for parsing markdown files, highlighting code snippets, Google Analytics, various GraphQL plugins, plus lots of development dependencies.&lt;/p&gt;
&lt;h3&gt;Dependencies...a lot of dependencies&lt;/h3&gt;
&lt;p&gt;I counted all the dependencies up before this rewrite &lt;strong&gt;29&lt;/strong&gt; third party dependencies in total. All this code that I needed to continue to upgrade and maintain for a simple blog that converts markdown in HTML.&lt;/p&gt;
&lt;p&gt;Now, I&apos;ve used Gatsby on a larger scale project at my last company and really enjoyed it. The plugin system and the community are pretty great. I&apos;ve even &lt;a href=&quot;https://github.com/gatsbyjs/gatsby/pull/21030&quot;&gt;contributed to Gatsby&apos;s source code&lt;/a&gt; in the past.&lt;/p&gt;
&lt;p&gt;You can find a plugin for practically anything. Just look up &lt;code&gt;gatsby-&lt;/code&gt; on &lt;a href=&quot;https://www.npmjs.com/search?q=gatsby-&quot;&gt;npm&lt;/a&gt; and look at the number of results: 5,212 (&lt;em&gt;as of 1/2/2022&lt;/em&gt;).&lt;/p&gt;
&lt;h3&gt;Why did I decide to rewrite the blog?&lt;/h3&gt;
&lt;p&gt;I&apos;ve been trying to go through my open source projects and get them all upgraded to the latest versions. I want all the security patches updated and make sure they&apos;re as relevant as possible. As part of that work, I looked at my own blog. I had a pretty old version of everything because I haven&apos;t upgraded any in over a year.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;I tried upgrading one package, but it required a newer version of Gatsby.&lt;/li&gt;
&lt;li&gt;So I upgraded Gatsby.&lt;/li&gt;
&lt;li&gt;Now all the other &lt;code&gt;gatsby-&lt;/code&gt; plugins broke, because they required the other version of Gatsby.&lt;/li&gt;
&lt;li&gt;I decided to just upgrade everything to the latest. The app wouldn&apos;t build because of some GraphQL issues. I checked the latest documentation and it was still using the code in the same way I was.&lt;/li&gt;
&lt;li&gt;I realized this dependency management is &lt;em&gt;way&lt;/em&gt; too much for a simple blog application and decided I needed something simpler.&lt;/li&gt;
&lt;li&gt;Remix has been on my mind for awhile now. I follow the creators on Twitter and have been wanting to try it out on a project.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Remix benefits&lt;/h3&gt;
&lt;p&gt;After rewriting this blog using Remix, I&apos;ve landed at &lt;strong&gt;15&lt;/strong&gt; third party dependencies. For those keeping track, that&apos;s about half the amount I had before.
I still have the same functionality as before: I&apos;m parsing markdown and turning it into HTML. I can highlight code snippets and I&apos;m deploying my app to Netlify.&lt;/p&gt;
&lt;p&gt;I still feel like 15 dependencies is a lot though. I really shouldn&apos;t need this many for a blog app, &lt;em&gt;but&lt;/em&gt; I feel like these 15 are way easier to maintain.
I have separate plugins that do their little piece well. It&apos;s feels like following the &lt;a href=&quot;https://en.wikipedia.org/wiki/Unix_philosophy&quot;&gt;Unix Philosophy&lt;/a&gt; and it&apos;s truly a breath of fresh air.
I have a single library just for parsing markdown into HTML. I have a single library that pulls frontmatter out of a markdown file.
These plugins can all be upgraded independently without breaking the others. Obviously, there are some plugins like React and Remix that have dependencies on each other, but nothing else does.&lt;/p&gt;
&lt;p&gt;This feels like a huge weight off my shoulders. We &lt;em&gt;all&lt;/em&gt; want to keep our applications up-to-date, but the &lt;a href=&quot;https://npmjs.com&quot;&gt;npm&lt;/a&gt; world makes that hard. Remix simplifies that for us.
It feels like I&apos;m working with plain JS again. There&apos;s certainly a lot of magic to Remix, but it&apos;s good magic. It&apos;s built in a way that you don&apos;t need to worry about the magic. You see and have the ability to customize where needed and can ignore all of the internals
that aren&apos;t relevant. It&apos;s extremely nice to work with. I do think Remix is headed in the right direction in terms of developer experience in the Javascript world.&lt;/p&gt;
</content:encoded></item><item><title>How to setup a dynamic sitemap.xml file on Netlify</title><link>https://www.riegle.dev/blogs/setting-up-dynamic-sitemap-file-on-netlify/</link><guid isPermaLink="true">https://www.riegle.dev/blogs/setting-up-dynamic-sitemap-file-on-netlify/</guid><pubDate>Sun, 02 Jan 2022 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;I recently migrated my site to use &lt;a href=&quot;https://remix.run&quot;&gt;Remix&lt;/a&gt; hosted on &lt;a href=&quot;https://netlify.com&quot;&gt;Netlify&lt;/a&gt;. I wanted to setup a dynamic &lt;code&gt;sitemap.xml&lt;/code&gt; file using Netlify functions. Remix already runs on a Netlify function, so I just created a separate function for returning the sitemap. Here&apos;s how I did it.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Create a new &lt;code&gt;sitemap&lt;/code&gt; folder in your &lt;code&gt;netlify/functions&lt;/code&gt; directory with an &lt;code&gt;index.ts&lt;/code&gt; file inside that directory.&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;mkdir netlify/functions/sitemap
touch netlify/functions/sitemap/index.ts
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;Inside your new &lt;code&gt;netlify/functions/sitemap/index.ts&lt;/code&gt; file, create a new handler that returns a basic XML file with no content.&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;import type { Handler } from &quot;@netlify/functions&quot;;

const handler: Handler = async () =&amp;gt; {
  const xmlContent = [
    &apos;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&amp;gt;&apos;,
    &apos;&amp;lt;urlset xmlns=&quot;http://www.sitemaps.org/schemas/sitemap/0.9&quot;&amp;gt;&apos;,
    &quot;&amp;lt;/urlset&amp;gt;&quot;,
  ];

  return {
    statusCode: 200,
    headers: { &quot;Content-Type&quot;: &quot;text/xml&quot; },
    body: xmlContent.join(&quot;\n&quot;),
  };
};

export { handler };
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;To test this out, let&apos;s add a redirect from &lt;code&gt;sitemap.xml&lt;/code&gt; to our newly created function. We can add it as a new redirect in the &lt;code&gt;netlify.toml&lt;/code&gt; file. You can read more about file based configuration with Netlify &lt;a href=&quot;https://docs.netlify.com/configure-builds/file-based-configuration/&quot;&gt;here&lt;/a&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;[build]
  command = &quot;remix build&quot;
  functions = &quot;netlify/functions&quot;
  publish = &quot;public&quot;

[dev]
  command = &quot;remix watch&quot;
  port = 3000

[[redirects]]
  from = &quot;/sitemap.xml&quot;
  to = &quot;/.netlify/functions/sitemap&quot;
  status = 200

[[redirects]]
  from = &quot;/*&quot;
  to = &quot;/.netlify/functions/server&quot;
  status = 200

[[headers]]
  for = &quot;/build/*&quot;
  [headers.values]
    &quot;Cache-Control&quot; = &quot;public, max-age=31536000, s-maxage=31536000&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Start up &lt;code&gt;netlify dev&lt;/code&gt; and make a request to &lt;code&gt;localhost:&amp;lt;YOUR PORT&amp;gt;/sitemap.xml&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;You should see a basic XML file returned:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&amp;gt;
&amp;lt;urlset xmlns=&quot;http://www.sitemaps.org/schemas/sitemap/0.9&quot;&amp;gt;
&amp;lt;/urlset&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;Add your URLs to the &lt;code&gt;sitemap.xml&lt;/code&gt; file dynamically using Javascript.&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;import type { Handler } from &quot;@netlify/functions&quot;;

const urls: Array&amp;lt;{ url: string; lastModified: string }&amp;gt; = [
  {
    url: &quot;https://riegle.dev/&quot;,
    lastModified: &quot;2022-01-01&quot;,
  },
  {
    url: &quot;https://riegle.dev/blogs&quot;,
    lastModified: &quot;2022-01-01&quot;,
  },
];

const handler: Handler = async () =&amp;gt; {
  const xmlContent = [
    &apos;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&amp;gt;&apos;,
    &apos;&amp;lt;urlset xmlns=&quot;http://www.sitemaps.org/schemas/sitemap/0.9&quot;&amp;gt;&apos;,
    ...urls.map(({ url, lastModified }) =&amp;gt; {
      return `
        &amp;lt;url&amp;gt;
          &amp;lt;loc&amp;gt;${url}&amp;lt;/loc&amp;gt;
          &amp;lt;lastmod&amp;gt;${lastModified}&amp;lt;/lastmod&amp;gt;
        &amp;lt;/url&amp;gt;
      `.trim();
    }),
    &quot;&amp;lt;/urlset&amp;gt;&quot;,
  ];

  return {
    statusCode: 200,
    headers: {
      &quot;Content-Type&quot;: &quot;text/xml&quot;,
    },
    body: xmlContent.join(&quot;\n&quot;),
  };
};

export { handler };
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;First, we define our site&apos;s URLs in an array with both the URL and the last modified date. These are the only properties I care about on my site, but you can feel free to add any additional fields you want from the &lt;a href=&quot;https://www.sitemaps.org/protocol.html&quot;&gt;XML sitemap specification&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;After we&apos;ve created our list of URLs, we can add it to our &lt;code&gt;xmlContent&lt;/code&gt; array by mapping each URL into an XML string.&lt;/p&gt;
&lt;h2&gt;Wrapping up&lt;/h2&gt;
&lt;p&gt;Make sure you add all your URLs into the URL list and keep it updated as you add additional URLs/update existing pages. This will ensure your pages gets indexed correctly by search engines.&lt;/p&gt;
</content:encoded></item><item><title>2022 goals</title><link>https://www.riegle.dev/blogs/2022-goals/</link><guid isPermaLink="true">https://www.riegle.dev/blogs/2022-goals/</guid><pubDate>Mon, 03 Jan 2022 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Man, it&apos;s 2022 already. Another year has passed, and I still suck at meeting my goals. 🤣 I want to walk through a quick review and talk about my new goals for this year.&lt;/p&gt;
&lt;p&gt;Feel free to check out my 2021 goals &lt;a href=&quot;/blogs/pilot/&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;2021 Goal Review&lt;/h2&gt;
&lt;p&gt;It&apos;s crucial to do a review of your previous goals. You may have assumptions about how well you did or did not do. Once you do a review, you can be confident of how you did and make improvements.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;[ ] Read at least one book per month.
&lt;ul&gt;
&lt;li&gt;I didn&apos;t accomplish this goal, but I read about three or four books this year.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;[ ] Write one blog post per week.
&lt;ul&gt;
&lt;li&gt;I didn&apos;t achieve this goal either. I wrote a single blog post this year after a freelance project. A total of 86 unique users visited my site this year.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;[x] Take more vacation time.
&lt;ul&gt;
&lt;li&gt;I did not do a good job defining this goal. At a minimum, I should have explicitly explained what &quot;more&quot; means here. Even though I did a poor job establishing the objective, I met this goal. I did a better job at taking time off. I want to try to keep this goal in 2022, but in a vivid way.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;[ ] Travel more
&lt;ul&gt;
&lt;li&gt;COVID continued to make this challenging in 2021. I had expected to do more traveling outdoors. I spent a significant amount of time outdoors, just not traveling.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;[ ] Upload one YouTube video per quarter.
&lt;ul&gt;
&lt;li&gt;I uploaded &lt;strong&gt;0&lt;/strong&gt; YouTube videos in 2022.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Takeaways&lt;/h3&gt;
&lt;p&gt;I need to do better with a few goals for 2022. One thing is being more explicit with my goals.
The examples of &quot;travel more&quot; and &quot;take more vacation time&quot; come to mind. Those are &lt;em&gt;agreeable&lt;/em&gt; goals, but they&apos;re hard to quantify at the end of the year.
Another thing is that I&apos;d like to follow a pattern for setting goals, in particular, the framework described by Sahil Bloom on &lt;a href=&quot;https://twitter.com/SahilBloom/status/1477307368534949890&quot;&gt;Twitter&lt;/a&gt;.
His tweet describes a precise structure for how to think about goal setting. I highly recommend it.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;2022 Goals&lt;/h2&gt;
&lt;p&gt;Alright, on to the substance of this blog post. Let&apos;s talk about the goals I&apos;m setting this year. I&apos;m tracking the same goal categories that Sahil mentioned in his thread: Personal, Professional, and Health.&lt;/p&gt;
&lt;h3&gt;Personal&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;BHAG&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;My Big Hairy Audacious Goal is to gain a significant Twitter and LinkedIn following, so I can help others learn to code and grow as software engineers. I aim to reach 1,000 connections on LinkedIn and 2,000 followers on Twitter.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Connected medium-term goal&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;My connected medium-term goal is to have &lt;strong&gt;50&lt;/strong&gt; followers on Twitter and &lt;strong&gt;350&lt;/strong&gt; connections on LinkedIn by February 15th.
&lt;ul&gt;
&lt;li&gt;I currently have 335 connections on LinkedIn and 39 followers on Twitter. To accomplish my medium-term goal, I&apos;ll need to gain one follower every four days and a new connection every three days on LinkedIn.&lt;/li&gt;
&lt;li&gt;Once February 15th comes, I&apos;ll create a new medium-term goal with a new timeline, whether I&apos;ve accomplished it or not.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Process Goals&lt;/strong&gt;
&lt;ol&gt;
&lt;li&gt;Write a Twitter post creating value each day, including following others and commenting on related posts.&lt;/li&gt;
&lt;li&gt;Write a meaningful LinkedIn post once per week, connect with others and comment on related posts daily.&lt;/li&gt;
&lt;li&gt;Write three blog posts per week.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;Professional&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;BHAG&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;My Big Hairy Audacious Goal is to grow as an engineer in Domain-Driven Development and Clean Code. By the end of the year, I want to give a conference talk on a related subject.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Connected medium-term goal&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;My connected medium-term goal is to write a talk on Domain-Driven Design by March 31st, 2022.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Process Goals&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Study the &amp;lt;a target=&quot;_blank&quot; href=&quot;https://www.amazon.com/gp/product/0321125215/ref=as_li_tl?ie=UTF8&amp;amp;camp=1789&amp;amp;creative=9325&amp;amp;creativeASIN=0321125215&amp;amp;linkCode=as2&amp;amp;tag=riegleblog-20&amp;amp;linkId=31e3146555db0b8880081150e1630641&quot;&amp;gt;Domain-Driven Design&amp;lt;/a&amp;gt; book by Eric Evans&lt;/li&gt;
&lt;li&gt;Study the &amp;lt;a target=&quot;_blank&quot; href=&quot;https://www.amazon.com/gp/product/0321834577/ref=as_li_tl?ie=UTF8&amp;amp;camp=1789&amp;amp;creative=9325&amp;amp;creativeASIN=0321834577&amp;amp;linkCode=as2&amp;amp;tag=riegleblog-20&amp;amp;linkId=1801f78bd75f2e2f1242259dca7ff1b9&quot;&amp;gt;Implementing Domain-Driven Design&amp;lt;/a&amp;gt; by Vaughn Vernon book.&lt;/li&gt;
&lt;li&gt;Write related blog posts and LinkedIn posts about the subject to get feedback from experts.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;Health&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;BHAG&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;My Big Hairy Audacious Goal is to weigh 200 lbs by the end of 2022. Currently, that&apos;s around 48 lbs to drop.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Connected medium-term goal&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;My connected medium-term goal is to weigh 244 lbs by February 1st, a loss of four pounds.
&lt;ul&gt;
&lt;li&gt;I&apos;ve read that one to two pounds per week is safe, so I&apos;m targeting that for my goals.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Process Goals&lt;/strong&gt;
&lt;ol&gt;
&lt;li&gt;16/8 Intermittent fasting at least 5 days per week
&lt;ul&gt;
&lt;li&gt;I&apos;ll track this in the &lt;a href=&quot;https://www.zerofasting.com/&quot;&gt;Zero&lt;/a&gt; app.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Cardio 5 days per week&lt;/li&gt;
&lt;li&gt;Strength training 3 days per week&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Goal Wrapping up&lt;/h2&gt;
&lt;p&gt;Happy New Year! I hope you have a great year. To our goals! 🍻&lt;/p&gt;
</content:encoded></item><item><title>February 2022 Health Goal Update</title><link>https://www.riegle.dev/blogs/february-2022-health-goal-update/</link><guid isPermaLink="true">https://www.riegle.dev/blogs/february-2022-health-goal-update/</guid><pubDate>Sat, 05 Feb 2022 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;I wanted to give a quick update on my health goal for the year.
Feel free to check out &lt;a href=&quot;/blogs/2022-goals/#health&quot;&gt;my previous blog post&lt;/a&gt; where I talked about the health goals I have set for 2022.
My long term goal is to weigh 200 lbs by the end of 2022.&lt;/p&gt;
&lt;p&gt;I started on January 3rd at &lt;strong&gt;248lbs.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Today, I&apos;m currently weighing in around &lt;strong&gt;245lbs.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;That puts me at about a loss of 1/2 lb per week. I still have 45.6 lbs left to reach my goal weight. If I stick to the 1/2 lb per week, it would take me until the end of 2023 to reach my goals. I don&apos;t want to wait that long.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;Right now, there are 47 weeks left in the year.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;If I push myself to lose 1 pound per week, I&apos;ll reach my goal by the end of the year.&lt;/li&gt;
&lt;li&gt;If I push myself to lose 2 pounds per week, I&apos;ll reach my goal by the middle of July.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I think it&apos;s time to push myself to lose 1.5 - 2 pounds per week. If I fail and only lose one pound, I&apos;ll still reach my goals.&lt;/p&gt;
&lt;h2&gt;New connected medium-term goal&lt;/h2&gt;
&lt;p&gt;I want to weigh 239 lbs by February 26th, 2022. That puts my target at two pounds per week for a total of three weeks.&lt;/p&gt;
&lt;p&gt;I &lt;em&gt;obviously&lt;/em&gt; need to make some changes to actually achieve this goal. Here&apos;s what I&apos;m doing.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;I&apos;m going to step up my calorie tracking. I have been using &lt;a href=&quot;https://www.myfitnesspal.com/&quot;&gt;MyFitnessPal&lt;/a&gt; for tracking calories,
but have recently slacked off on that. I stopped after a weekend where it was harder to keep track of meals. I&apos;m going to get back to using it every day.&lt;/li&gt;
&lt;li&gt;Increase my exercise time. I started strong with riding on an exercise bike every day and using free weights every other day, but have found myself slacking
in this area over the last few weeks. This happened after the off-weekend too. I&apos;m recommitting to doing this again.&lt;/li&gt;
&lt;li&gt;I just purchased James Clear&apos;s book about Atomic Habits.
I&apos;ve heard amazing things about this book and think it will be good for me to set small, achieveable goals in my daily life. I&apos;m committing to read this book over the next three weeks as well.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Weight Chart&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Date&lt;/th&gt;
&lt;th&gt;Weight&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;01/04/2022&lt;/td&gt;
&lt;td&gt;248.0 lbs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;01/10/2022&lt;/td&gt;
&lt;td&gt;247.8 lbs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;01/15/2022&lt;/td&gt;
&lt;td&gt;247.0 lbs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;01/20/2022&lt;/td&gt;
&lt;td&gt;246.0 lbs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;01/27/2022&lt;/td&gt;
&lt;td&gt;247.2 lbs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;01/30/2022&lt;/td&gt;
&lt;td&gt;245.6 lbs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;02/04/2022&lt;/td&gt;
&lt;td&gt;245.6 lbs&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
</content:encoded></item><item><title>How to get the count of a relationship using Prisma</title><link>https://www.riegle.dev/blogs/nested-relationship-counts-using-prisma/</link><guid isPermaLink="true">https://www.riegle.dev/blogs/nested-relationship-counts-using-prisma/</guid><pubDate>Wed, 19 Apr 2023 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Example Problem&lt;/h2&gt;
&lt;p&gt;Imagine we want to build a page to display all our posts including the number of comments on each post. Something like this...&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;./diagrams/posts-page.excalidraw&quot;&gt;Posts Page&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;Prisma Schema&lt;/h2&gt;
&lt;p&gt;Let&apos;s start off by defining a Prisma schema that we can use in our example. We&apos;ll create a &lt;code&gt;Post&lt;/code&gt; and &lt;code&gt;Comment&lt;/code&gt; table. Each post can have many comments on it.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;model Post {
  id        Int       @id @default(autoincrement())
  title     String
  body      String
  comments  Comment[]
  createdAt DateTime  @default(now())
}

model Comment {
  id        Int      @id @default(autoincrement())
  comment   String
  author    String
  post      Post     @relation(fields: [postId], references: [id])
  postId    Int
  createdAt DateTime @default(now())
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Code&lt;/h2&gt;
&lt;p&gt;You can use the &lt;code&gt;include&lt;/code&gt; property and the &lt;code&gt;_count&lt;/code&gt; property inside of &lt;code&gt;include&lt;/code&gt; to the get the count of any relationships.&lt;/p&gt;
&lt;p&gt;This will add a &lt;code&gt;_count&lt;/code&gt; property on your query results where you can get the count of the comments.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const prisma = new PrismaClient();

const postsWithCommentCount = await prisma.post.findMany({
  include: {
    _count: {
      select: {
        comments: true,
      }
    }
  }
})

return {
  posts: postsWithCommentCount.map((post) =&amp;gt; ({
    id: post.id,
    title: post.title,
    body: post.body,
    commentCount: post._count.comments
    createdAt: post.createdAt,
  }))
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>SaaS Journey - Update #1</title><link>https://www.riegle.dev/blogs/saas-update-1/</link><guid isPermaLink="true">https://www.riegle.dev/blogs/saas-update-1/</guid><pubDate>Mon, 24 Apr 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;I&apos;m on a journey to bootstrap a profitable SaaS application. I want to document it here for others and to hold myself accountable.
I&apos;ve been talking about doing this for years and feel like it&apos;s time that I &lt;em&gt;have to&lt;/em&gt; make it happen. I&apos;ve made a change in my career to free up time
so I can focus on building my own business.&lt;/p&gt;
&lt;p&gt;I have had the hardest time with focusing on a single idea. I think of an idea, find a competitor and just &lt;em&gt;quit&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;I&apos;ve done that a countless number of times, but this time needs to be different.&lt;/p&gt;
&lt;p&gt;I just watched a video where the guy did a 100 day challenge to build a SaaS app. It ultimately took longer than that, but they learned
a ton and went on to build a successful software company afterwards.&lt;/p&gt;
&lt;p&gt;Even if this first product idea sucks and fails, I&apos;m going to keep working, listening to feedback and adjusting as needed.&lt;/p&gt;
&lt;p&gt;I need to get used to shipping a product and seeing it through even if there are stumbling blocks in the process.&lt;/p&gt;
&lt;h2&gt;My first SaaS idea&lt;/h2&gt;
&lt;p&gt;A questionnaire/survey builder for small-to-medium size businesses for operators to easily create reusable templates for their employees. There are a million spreadsheets,
paper templates, word documents, etc where people do this today. There are massive competitors in this space but I don&apos;t see a lot targeted at this area of the market.&lt;/p&gt;
&lt;p&gt;One of the &lt;a href=&quot;https://play.google.com/store/apps/details?id=com.wbpartsexpress.ali&amp;amp;hl=en_US&amp;amp;gl=US&quot;&gt;first products&lt;/a&gt; I worked on as a developer had a feature like this. It was focused on the Aircraft Maintenance niche. You could use premade reports or build your own inspection types.&lt;/p&gt;
&lt;p&gt;I&apos;m planning to build this in a generic way for any business type to easily build and manage questionnaires for their business. They&apos;ll be able to invite employees to the app to complete them. This will mainly be for internal use.
I need to simplify the features and map out an MVP for this so I don&apos;t spend 6 months trying to build something that nobody wants to use.&lt;/p&gt;
&lt;h2&gt;MVP Plan&lt;/h2&gt;
&lt;p&gt;I&apos;m trying to balance the idea of building something too simple and actually providing value. I want to build the smallest example
product that is useful. I don&apos;t necessarily even want to launch something that somebody would buy. I just want to get something useful launched.&lt;/p&gt;
&lt;p&gt;If I can build a &lt;em&gt;very simple&lt;/em&gt; product that is useful, I can later add all the advanced features like authentication, reports, user roles, image uploads, mobile application, etc.&lt;/p&gt;
&lt;p&gt;My current plan is to build a simple landing page that instantly allows you to start building a survey. You&apos;ll be able to save your surveys
using your email address. I&apos;ll store them in your local storage too. (Very simple)&lt;/p&gt;
&lt;p&gt;I could build the survey as a JSON format, base64 encode it and use that as the share link.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;No authentication&lt;/li&gt;
&lt;li&gt;No complex user roles&lt;/li&gt;
&lt;li&gt;No database&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The problem though...is this even useful? Will it be so damn simple that it&apos;s useless? Probably.
Should I still do it? I don&apos;t know.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Here&apos;s an overly simplified example sketch of what I&apos;m thinking.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;./diagrams/saas-1/landing-page.excalidraw&quot;&gt;Landing Page Example&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>Easily hide/modify output using tRPC middleware</title><link>https://www.riegle.dev/blogs/modify-query-output-with-trpc-middleware/</link><guid isPermaLink="true">https://www.riegle.dev/blogs/modify-query-output-with-trpc-middleware/</guid><pubDate>Mon, 22 May 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;When using tRPC, it&apos;s easy to run into a problem of duplicating output logic to try to hide/modify/change the output consistently.
I&apos;m going to show you how you can use tRPC middleware to dedupe that logic in a clean and reusable way.&lt;/p&gt;
&lt;p&gt;Let&apos;s imagine your application has &lt;strong&gt;Projects&lt;/strong&gt; and &lt;strong&gt;Users&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;We have two types of Users (&lt;em&gt;admins&lt;/em&gt; and &lt;em&gt;members&lt;/em&gt;).&lt;/p&gt;
&lt;p&gt;For simplicity, let&apos;s say we only want to return project &lt;em&gt;descriptions&lt;/em&gt; for admins. Members cannot view descriptions.&lt;/p&gt;
&lt;h2&gt;Simplest solution&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;export const projectsRouter = createTrpcRouter({
  getProjects: publicProcedure.query(async ({ ctx }) =&amp;gt; {
    const isAdmin = ctx.user.type === &quot;admin&quot;;
    const projects = await ctx.prisma.projects.findMany();

    return projects.map((project) =&amp;gt; {
      return {
        ...project,
        description: isAdmin ? project.description : &quot;&quot;,
      };
    });
  }),
  getProjectById: publicProcedure
    .input(z.object({ id: z.string() }))
    .query(async ({ ctx, input }) =&amp;gt; {
      const isAdmin = ctx.user.type === &quot;admin&quot;;
      const project = await ctx.prisma.projects.findFirstOrThrow({ where: { id: input.id } });

      return {
        ...project,
        description: isAdmin ? project.description : &quot;&quot;,
      };
    }),
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Did you notice the duplication? 😱&lt;/p&gt;
&lt;h2&gt;Extracting shared logic&lt;/h2&gt;
&lt;p&gt;Let&apos;s try to refactor some of this logic so there&apos;s less duplicated code.&lt;/p&gt;
&lt;p&gt;We&apos;ll create a &lt;code&gt;mapProject&lt;/code&gt; function that takes in a project and whether the user is an admin and decides
how to map the project.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import type { Project } from &apos;@prisma/client&apos;;

const mapProject = (project: Project, isAdmin: boolean) =&amp;gt; ({
  ...project,
  description: isAdmin ? project.description : &apos;&apos;,
});

export const projectsRouter = createTrpcRouter({
  getProjects: publicProcedure.query(async ({ ctx }) =&amp;gt; {
    const isAdmin = ctx.user.type === &quot;admin&quot;;
    const projects = await ctx.prisma.projects.findMany();
    return projects.map((project) =&amp;gt; mapProject(project, isAdmin));
  }),
  getProjectById: publicProcedure
    .input(z.object({ id: z.string() }))
    .query(async ({ ctx, input }) =&amp;gt; {
      const isAdmin = ctx.user.type === &quot;admin&quot;;
      const project = await ctx.prisma.projects.findFirstOrThrow({ where: { id: input.id } });
      return mapProject(project, isAdmin);
    }),
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Using middleware to define the function with &quot;context&quot;&lt;/h2&gt;
&lt;p&gt;Alright, this is starting to look a little cleaner. Let&apos;s introduce &quot;middleware&quot; into the mix
to see how this can help us clean up some more code.&lt;/p&gt;
&lt;p&gt;Instead of creating the &lt;code&gt;mapProject&lt;/code&gt; function outside of tRPC and using it, we can actually create it within our tRPC middleware.
It will have all the information from the request that we need to make a decision. In this case, we can check if the user is an admin
and decide if we should return a &quot;description&quot; from within the middleware.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import type { Project } from &apos;@prisma/client&apos;;

const projectProcedure = publicProcedure.use(({ ctx, next }) =&amp;gt; {
  return next({
    ctx: {
      mapProject: (project: Project) =&amp;gt; {
        const isAdmin = ctx.user.type === &quot;admin&quot;;

        return {
          ...project,
          description: isAdmin ? project.description : &apos;&apos;,
        };
      },
    }
  })
});

export const projectsRouter = createTrpcRouter({
  getProjects: projectProcedure.query(async ({ ctx }) =&amp;gt; {
    const projects = await ctx.prisma.projects.findMany();
    return projects.map(ctx.mapProject);
  }),
  getProjectById: projectProcedure
    .input(z.object({ id: z.string() }))
    .query(async ({ ctx, input }) =&amp;gt; {
      return ctx.mapProject(
        await ctx.prisma.projects.findFirstOrThrow({ where: { id: input.id } })
      );
    }),
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Using tRPC middleware to it&apos;s full potential&lt;/h2&gt;
&lt;p&gt;We can actually do the entire mapping within the middleware so we don&apos;t need to even think
about it inside of queries/mutations. Imagine a new engineer joins the project, you don&apos;t want to have to remind them to use the &lt;code&gt;ctx.mapProject&lt;/code&gt; function.&lt;/p&gt;
&lt;p&gt;Maybe you&apos;re on vacation and they accidentally ship code that reveals the description to all members. That would be horrible.&lt;/p&gt;
&lt;p&gt;Let&apos;s see how tRPC middleware can solve this for us in a clean, reusable way.&lt;/p&gt;
&lt;p&gt;First...our updated router. Look how simple this is.&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export const projectsRouter = createTrpcRouter({
  getProjects: publicProcedure.query(async ({ ctx }) =&amp;gt; ctx.prisma.projects.findMany(),
  getProjectById: publicProcedure
    .input(z.object({ id: z.string() }))
    .query(({ ctx, input }) =&amp;gt; ctx.prisma.projects.findFirstOrThrow({ where: { id: input.id } })),
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Already, here&apos;s the moment we&apos;ve all been waiting for...&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const isProject = (project: unknown): project is Project =&amp;gt; false; // needs to be implemented
const isProjects = (projects: unknown): projects is Project[] =&amp;gt; Array.isArray(projects) &amp;amp;&amp;amp; projects.every(isProject);

const projectProcedure = publicProcedure.use(({ ctx, next }) =&amp;gt; {
  const result = await next();

  if (!result.ok) {
    return result;
  }

  const mapProject = (project: Project) =&amp;gt;  {
    const isAdmin = ctx.user.type === &quot;admin&quot;;

    return {
      ...project,
      description: isAdmin ? project.description : &apos;&apos;,
    }
  };

  if (isProjects(result.data) || isProject(result.data)) {
    return {
      ...result,
      data: isProjects(result.data)
        ? result.data.map((project) =&amp;gt; mapProject(project))
        : mapProject(result.data),
    };
  }

  return result;
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In our tRPC middleware, we can actually call the &lt;code&gt;next()&lt;/code&gt; function and await the result. This will return the result from the query/mutation
which we can use before returning the value to the client.&lt;/p&gt;
&lt;p&gt;We await the result, make sure it was successful and then we check the return type to see if it is a project or list of projects, if so, we call the &lt;code&gt;mapProject&lt;/code&gt;
on each project to hide the description for non-admin users.&lt;/p&gt;
&lt;h2&gt;Full Example&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;import type { Project } from &apos;@prisma/client&apos;;
import { createTrpcRouter, publicProcedure } from &apos;../trpc&apos;;

const isProject = (project: unknown): project is Project =&amp;gt; false; // needs to be implemented
const isProjects = (projects: unknown): projects is Project[] =&amp;gt; Array.isArray(projects) &amp;amp;&amp;amp; projects.every(isProject);

const projectProcedure = publicProcedure.use(({ ctx, next }) =&amp;gt; {
  const result = await next();

  if (!result.ok) {
    return result;
  }

  const mapProject = (project: Project) =&amp;gt;  {
    const isAdmin = ctx.user.type === &quot;admin&quot;;

    return {
      ...project,
      description: isAdmin ? project.description : &apos;&apos;,
    }
  };

  if (isProjects(result.data) || isProject(result.data)) {
    return {
      ...result,
      data: isProjects(result.data)
        ? result.data.map((project) =&amp;gt; mapProject(project))
        : mapProject(result.data),
    };
  }

  return result;
});

export const projectsRouter = createTrpcRouter({
  getProjects: publicProcedure.query(async ({ ctx }) =&amp;gt; ctx.prisma.projects.findMany(),
  getProjectById: publicProcedure
    .input(z.object({ id: z.string() }))
    .query(({ ctx, input }) =&amp;gt; ctx.prisma.projects.findFirstOrThrow({ where: { id: input.id } })),
});
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Caching &amp; change detection  Turborepo and Github Actions</title><link>https://www.riegle.dev/blogs/setup-caching-in-turborepo-with-github-actions/</link><guid isPermaLink="true">https://www.riegle.dev/blogs/setup-caching-in-turborepo-with-github-actions/</guid><pubDate>Thu, 01 Jun 2023 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;&quot;Remote Caching&quot;&lt;/h2&gt;
&lt;p&gt;I recently worked on a project with Turborepo and wanted to setup caching without using &lt;a href=&quot;https://turbo.build/repo/docs/core-concepts/remote-caching&quot;&gt;Vercel&apos;s proprietary Remote Caching&lt;/a&gt;.
I also wanted change detection to only deploy things that have changed. Here&apos;s how I did it.&lt;/p&gt;
&lt;p&gt;First, let&apos;s just setup a basic CI workflow that runs our test suite.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;name: ci
on:
  pull_request:
    types: [opened, synchronize]

jobs:
  test:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout code
        uses: actions/checkout@v3
        with:
          fetch-depth: 2
      
      - name: Setup Node
        uses: actions/setup-node@v3
        with:
          node-version: 18
          cache: &apos;npm&apos;
      
      - run: npm ci
      - run: npx turbo run test
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This file will run our tests, but does &lt;em&gt;not&lt;/em&gt; setup any turborepo caching.&lt;/p&gt;
&lt;p&gt;Here&apos;s how we add the turborepo cache:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Make sure&lt;/strong&gt; you add the &lt;code&gt;--cache-dir&lt;/code&gt; argument to your test call. This will tell turborepo where to save and restore your cache files.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;name: ci
on:
  pull_request:
    types: [opened, synchronize]

jobs:
  test:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout code
        uses: actions/checkout@v3
        with:
          fetch-depth: 2

      - name: Cache turborepo
        uses: actions/cache@v3
        with:
          path: .turbo
          key: ${{ runner.os }}-turbo-${{ github.sha }}
          restore-keys: |
            ${{ runner.os }}-turbo-
      
      - name: Setup Node
        uses: actions/setup-node@v3
        with:
          node-version: 18
          cache: &apos;npm&apos;
      
      - run: npm ci
      - run: npx turbo run test --cache-dir=.turbo
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Change Detection&lt;/h2&gt;
&lt;p&gt;Alright, we have that setup, but how do we do change detection for deployments? There are honestly several ways to do this but the most generic and simplest
way is to use turborepo to detect &lt;em&gt;if&lt;/em&gt; a package has changed and deploy it.&lt;/p&gt;
&lt;p&gt;Let&apos;s start with defining our own custom Github Action that we can use within our repo for change detection.&lt;/p&gt;
&lt;p&gt;You &lt;em&gt;do&lt;/em&gt; have to put it within a folder and the file &lt;em&gt;must&lt;/em&gt; be called _action.yml`&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;name: Has Changed?
description: Checks if a Turborepo Workspace has changed
inputs:
  workspace_name:
    required: true
    description: |-
      Name of Turborepo workspace
  from_ref:
    required: true
    description: |-
      Github Ref to detect changes from
  to_ref:
    required: true
    description: |-
      Github ref to detect changes to
  cache_dir:
    required: false
    default: .turbo
    description: |-
      Custom cache directory for turborepo
  turbo_version:
    required: false
    default: 1.9.3
    description: |-
      Turborepo version
  force:
    required: false
    default: false
    description: |-
      Used to force this action to return true

outputs:
  changed:
    description: |-
      &apos;true&apos; or &apos;false&apos; value indicating whether the workspace changed
    value: ${{ steps.turbo_check_changed.outputs.changed }}
runs:
  using: &apos;composite&apos;
  steps:
    - name: Setup Node.js environment
      uses: actions/setup-node@v3
      with:
        node-version: 18
        cache: &apos;npm&apos;
    - run: npm install -g turbo@${{ inputs.turbo_version }}
      shell: bash
    - id: turbo_check_changed
      shell: bash
      run: |
        if [[ &quot;${{ inputs.force }}&quot; == &apos;true&apos; ]]; then
          echo &quot;changed=true&quot; &amp;gt;&amp;gt; $GITHUB_OUTPUT
        else
          HAS_CHANGED=$(npx turbo build --cache-dir=${{ inputs.cache_dir }} --filter=&quot;${{ inputs.workspace_name }}...[${{ inputs.from_ref }}...${{ inputs.to_ref }}]&quot; --dry-run=json | jq &quot;.packages|any(. == \&quot;${{ inputs.workspace_name }}\&quot;)&quot;)
          echo &quot;changed=${HAS_CHANGED}&quot; &amp;gt;&amp;gt; $GITHUB_OUTPUT
        fi
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This code gets pretty complex in the last bash script. Basically what we are doing is &lt;code&gt;npx turbo build&lt;/code&gt; and having it run a &quot;dry run&quot; of the build. It won&apos;t actually build the packages, it is just going to tell you what it will do.
We set the output to json using the &lt;code&gt;--dry-run=json&lt;/code&gt; argument. This allows us to detect which packages would be built and we can assume have changed.&lt;/p&gt;
&lt;p&gt;We use the &lt;code&gt;&quot;echo changed=&amp;lt;value&amp;gt;&quot;&quot; &amp;gt;&amp;gt; $GITHUB_OUTPUT&lt;/code&gt; syntax to set the output for the action. The output can be used in our deployment workflow to decide if we should deploy it. Let&apos;s dive into that code.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;name: ci
on:
  push:
    branches: [&apos;main&apos;]
  workflow_dispatch:

concurrency:
  group: ${{ github.workflow }}
  cancel-in-progress: false

jobs:
  ci:
    uses: ./.github/workflows/ci.yml # use our exisiting CI workflow to run tests

  deploy:
    runs-on: ubuntu-latest
    needs: [ci] # make sure our tests pass before trying to deploy

    steps:
      - name: Checkout code
        uses: actions/checkout@v3
        with:
          fetch-depth: 2

      - id: has-changed
        uses: ./.github/actions/has-changed # note: this the folder name we stored the &quot;action.yml&quot; file in earlier
        with:
          workspace_name: &amp;lt;your-package.json-name&amp;gt;
          from_ref: ${{ github.ref_name }}
          to_ref: HEAD^1
          force: ${{ github.event_name == &apos;workflow_dispatch&apos; }} # this flag forces the package to be marked as changed so it gets deployed. This is useful when you manually trigger a deploy like with a workflow_dispatch event

      - name: Cache turborepo
        if: steps.has-changed.outputs.changed == &apos;true&apos; # we only run this if the package has changed
        uses: actions/cache@v3
        with:
          path: .turbo
          key: ${{ runner.os }}-turbo-${{ github.sha }}
          restore-keys: |
            ${{ runner.os }}-turbo-
      
      - name: Setup Node
        if: steps.has-changed.outputs.changed == &apos;true&apos;
        uses: actions/setup-node@v3
        with:
          node-version: 18
          cache: &apos;npm&apos;
      
      - run: npm ci
        if: steps.has-changed.outputs.changed == &apos;true&apos;

      - run: &amp;lt;insert your deploy command&amp;gt;
        if: steps.has-changed.outputs.changed == &apos;true&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;There is a lot here to unpack, but this workflow will only deploy if our code has actually changed. It also allows us to use Github&apos;s workflow dispatch feature to force deployments if we need to redeploy for some reason.&lt;/p&gt;
&lt;p&gt;Let me know what you think or if you have any suggestions to make this better!&lt;/p&gt;
</content:encoded></item></channel></rss>