diff --git a/jaysherby.com/a-formal-letter-of-apology-to-batman/index.html b/jaysherby.com/a-formal-letter-of-apology-to-batman/index.html index da68b93..2309778 100644 --- a/jaysherby.com/a-formal-letter-of-apology-to-batman/index.html +++ b/jaysherby.com/a-formal-letter-of-apology-to-batman/index.html @@ -1,17 +1,26 @@ + - - -A formal letter of apology to Batman | Jay's blog - - + + + + + + A formal letter of apology to Batman | Jay's blog + + + + + + + @@ -21,6 +30,8 @@ I hope this letter finds you well. It is with deep remorse and a heavy heart that I pen this formal apology to you. Recently, a rather unplea..."> + + @@ -29,6 +40,9 @@ I hope this letter finds you well. It is with deep remorse and a heavy heart that I pen this formal apology to you. Recently, a rather unplea..."> + + + - - - + + + -
- -

-Jay's blog -

-
- -
-
-

A formal letter of apology to Batman

-

- - - -

-

Dear Batman,

+ +
+ +

+ Jay's blog +

+
+ +
+
+ + + + + + + +

A formal letter of apology to Batman

+ +

+ + + +

+ + +

Dear Batman,

I hope this letter finds you well. It is with deep remorse and a heavy heart that I pen this formal apology to you. Recently, a rather unpleasant incident occurred within the confines of the sacred Bat Cave, an incident for which I must take full responsibility. I am truly sorry to inform you that I, Robin, was the culprit behind the unfortunate emission of flatulence in our esteemed headquarters.

As a trusted member of the Dynamic Duo and your loyal crime-fighting companion, I understand the solemnity and sanctity of our crime-fighting operations. The Bat Cave serves as the epicenter of our collaborative efforts, where we strategize, analyze, and prepare for the challenges that await us in the perilous streets of Gotham City. It is a place of utmost professionalism, focus, and, of course, fresh air.

The events leading up to the aforementioned mishap were rather innocuous. In hindsight, I admit to having consumed a hearty meal of beans, onions, and broccoli before our mission. My intentions were pure, as I sought to replenish my energy reserves for the arduous night of crime-fighting that lay ahead. Alas, the subsequent effects were unforeseen and regrettable.

@@ -336,19 +382,33 @@

A formal letter of apology to Batman

Once again, Batman, I extend my sincere apologies for defiling the sanctity of the Bat Cave with my untimely flatulence. I hope you can find it in your heart to forgive me, and that our partnership may continue with the same unwavering strength and camaraderie as before.

With the sincerest apologies and the utmost respect,

Robin

-
- - - - - + + + + + + + + + + + + + + +
+ -
- + + + + +
+ \ No newline at end of file diff --git a/jaysherby.com/back-that-blog-up/index.html b/jaysherby.com/back-that-blog-up/index.html index ebebea5..03a8fe6 100644 --- a/jaysherby.com/back-that-blog-up/index.html +++ b/jaysherby.com/back-that-blog-up/index.html @@ -1,17 +1,26 @@ - - - -Back That Blog Up | Jay's blog - - + + + + + + + Back That Blog Up | Jay's blog + + + + + + + + @@ -21,6 +30,8 @@ There was a post on Hacker News about a week ago titled . As someone who has tried to blog for years but only recently started doing it in earnest, a lot..."> + + @@ -29,6 +40,9 @@ There was a post on Hacker News about a week ago titled . As someone who has tried to blog for years but only recently started doing it in earnest, a lot..."> + + + - - - - + + + -
- -

-Jay's blog -

-
- -
-
-

Back That Blog Up

-

- - - -

-

Original art, The Sherbmeister. Makin' copies.

-

There was a post on Hacker News about a week ago titled "Ask HN: What has your personal website/blog done for you?". As someone who has tried to blog for years but only recently started doing it in earnest, a lot of comments rang true to me.

+ +
+ +

+ Jay's blog +

+
+ +
+
+ + + + + + + +

Back That Blog Up

+ +

+ + + +

+ + +

Original art, The Sherbmeister. Makin' copies.

+

There was a post on Hacker News about a week ago titled "Ask HN: What has your personal website/blog done for you?". As someone who has tried to blog for years but only recently started doing it in earnest, a lot of comments rang true to me.

I, too, have bounced off of many static site generators. While I strongly agree with the ethos behind static site generators, I find the barrier to writing to be too high. When I'm in the mood to write, I need to be able to load up a web page with a text box and just start writing.

-

Bear Blog, the service I'm currently using to host this site, provides exactly what I need. Kudos to Herman Martinus for developing and maintaining this fantastic service.

-

There was something else in that Hacker News thread that made me a little nervous, though. A particular comment mentioned keeping copies of their blog posts, which came in handy when their platform of choice shut down. Many others echoed this sentiment of keeping backups of your work, just in case.

+

Bear Blog, the service I'm currently using to host this site, provides exactly what I need. Kudos to Herman Martinus for developing and maintaining this fantastic service.

+

There was something else in that Hacker News thread that made me a little nervous, though. A particular comment mentioned keeping copies of their blog posts, which came in handy when their platform of choice shut down. Many others echoed this sentiment of keeping backups of your work, just in case.

Herman is doing a fantastic job, but he's only one person, and shit happens sometimes. I decided build a little safety net by using GitHub Actions to automatically back up my blog.

-

A shell script

-View the complete shell script -

.github/workflows/backup.sh

#! /bin/sh
+

A shell script

+ View the complete shell script +

.github/workflows/backup.sh

#! /bin/sh
 
 set -e
 set -u
@@ -383,14 +428,14 @@ 

.github/workflows/backup.sh -e robots=off \ https://jaysherby.com

-

Most of these options are copy-pasted from an article I found called "How To Download A Website With Wget The Right Way". I prefer having the options spelled out long-hand in scripts like this over single character arguments. It's easier to understand.

+

Most of these options are copy-pasted from an article I found called "How To Download A Website With Wget The Right Way". I prefer having the options spelled out long-hand in scripts like this over single character arguments. It's easier to understand.

I made a few changes from the example on the site I linked to above.

  • I removed the --user-agent argument. Wget's default is fine.
  • I removed the --no-clobber argument because it's not pertinent in my case for reasons that will become clear in a moment.
  • I removed the --limit-rate=20K argument because although I understand the purpose of setting --wait=2 to avoid overwhelming the server with a ton of requests all at once, it seems to me like limiting the transfer speed would end up wasting precious time that could be spent serving other clients. It really doesn't matter for the amount of traffic I get anyway.
  • I added --span-hosts and --domains="jaysherby.com,digitaloceanspaces.com" to ensure images are downloaded. Bear Blog is currently hosted on Digital Ocean and images are stored in their Spaces product, which is more or less a clone of AWS S3. This feature is currently configured such that image URLs live in a subdomain of digitaloceanspaces.com.
  • -
  • I added --exclude-directories="hit,upvote". Wget was downloading files from /hit which is how Bear Blog powers its analytics service. It's currently implemented by setting URLs in that path as a border image applied to the page body on hover. Clever! But it currently only returns a response of "Logged", which is useless for my backups and would present as noise in my analytics. The /upvote path is how posts are "toasted". Wget should never follow these by default since they are form actions, but better safe than sorry.
  • +
  • I added --exclude-directories="hit,upvote". Wget was downloading files from /hit which is how Bear Blog powers its analytics service. It's currently implemented by setting URLs in that path as a border image applied to the page body on hover. Clever! But it currently only returns a response of "Logged", which is useless for my backups and would present as noise in my analytics. The /upvote path is how posts are "toasted". Wget should never follow these by default since they are form actions, but better safe than sorry.
# Delete CSRF tokens since they'll change every page load.
 # Delete last build date tag since it will change often.
@@ -405,7 +450,7 @@ 

.github/workflows/backup.shThe second is the last build date tag that is found within my blog's RSS file. This is updated on a regular basis whether any content has changed or not. Again, this would make for a lot of noise in my backups.

The final pattern is an "updated" tag that seems to always contain the timestamp when the page was generated. These are present on pages in the /feed URL path.

Notice in the second sed command that the slash in the HTML tag is escaped! I originally overlooked this and it bit me. 🤦

-

Quick shout out to this website that is like Regex 101 for sed. It saved me a lot of time testing out my sed commands.

+

Quick shout out to this website that is like Regex 101 for sed. It saved me a lot of time testing out my sed commands.

# Detect any added, changed, or deleted files
 if [ -n "$(git ls-files --modified --deleted --others)" ]; then
   git add -A
@@ -413,7 +458,7 @@ 

.github/workflows/backup.sh git push origin main fi

-

This does what it says on the tin. It uses the git ls-files command to determine if any files have changed. If so, it makes a commit with the current timestamp in the commit message and pushes it up.

+

This does what it says on the tin. It uses the git ls-files command to determine if any files have changed. If so, it makes a commit with the current timestamp in the commit message and pushes it up.

Let's skip back to the beginning of the script.

# Delete all directories that are not hidden to avoid keeping deleted files.
 find . -maxdepth 1 -type d -not -name '.*' -exec rm -r "{}" \;
@@ -422,9 +467,9 @@ 

.github/workflows/backup.shThis is to facilitate the removal of potentially deleted pages and files. For the purposes of my backups, I want the git history to reflect file deletion since I can always recover deleted content.

Unfortunately, Wget doesn't have anything built into it like Rsync's various --delete arguments, which will make sure your source and destination match exactly after synchronization by deleting files from the destination that no longer exist at the source.

I find this particularly unfortunate because it means having to download my blog's images on every backup attempt despite Wget's automatic If-Modified-Since header support and Digital Ocean Spaces' 304 Not Modified support. Bear Blog itself doesn't appear to support If-Modified-Since request headers at this time. Although that doesn't really matter since I can't leverage it anyway.

-

A GitHub Actions workflow

-View the complete YAML file -

.github/workflows/backup.yml

name: Back Up Website
+

A GitHub Actions workflow

+ View the complete YAML file +

.github/workflows/backup.yml

name: Back Up Website
 
 on:
   schedule:
@@ -459,7 +504,7 @@ 

.github/workflows/backup.yml workflow_dispatch:

-

Run the script daily at noon Central Standard Time, my local time zone, so if it ever fails, I get emailed about it at a reasonable time. GitHub Actions are set to Coordinated Universal Time, hence the offset. I used this website to refresh my memory of cron expression syntax.

+

Run the script daily at noon Central Standard Time, my local time zone, so if it ever fails, I get emailed about it at a reasonable time. GitHub Actions are set to Coordinated Universal Time, hence the offset. I used this website to refresh my memory of cron expression syntax.

The workflow_dispatch key is there so I can run the job manually via the web interface if I need to.

jobs:
   back-up:
@@ -473,18 +518,18 @@ 

.github/workflows/backup.yml sudo apt-get install -y findutils git sed wget

The first step checks out the git repository. The second step installs all required software to run the script. All of these tools are currently installed by default in the ubuntu-latest job runner. Better safe than sorry, though.

-

Remember to use sudo to install software in GitHub's ubuntu-latest job runner, unlike the usual situation for Docker containers. This one bit me.

+

Remember to use sudo to install software in GitHub's ubuntu-latest job runner, unlike the usual situation for Docker containers. This one bit me.

- name: Configure identity
   run: |
     git config --global user.email "10983817+hexpunk@users.noreply.github.com"
     git config --global user.name "hexpunk"
 
-

I was surprised to find out the job runner didn't have any git identity set up by default. I added my own username and GitHub's email alias for my account. If you're not using your email alias on GitHub, you really should be.

+

I was surprised to find out the job runner didn't have any git identity set up by default. I added my own username and GitHub's email alias for my account. If you're not using your email alias on GitHub, you really should be.

- name: Run backup script
   run: sh $GITHUB_WORKSPACE/.github/workflows/backup.sh
 

Finally, run the script I wrote.

-

Setting permissions

One last consideration before this setup is ready to roll. Because the shell script is checking files into git, I had to set up the repository's Actions settings accordingly.

+

Setting permissions

One last consideration before this setup is ready to roll. Because the shell script is checking files into git, I had to set up the repository's Actions settings accordingly.

Give Actions read and write permissions.

The repository I set up for this is public. GitHub sets a monthly limit to the amount of time Actions can run per month on private repositories before you're either cut off or you have to pay. I don't want randos taking advantage of this to run malicious actions, so I set this setting to disallow unapproved collaborators from triggering Actions.

Strangers shouldn't be able to run Actions on this repository.

@@ -493,24 +538,38 @@

Setting permissions

One last consideration b

This setting is a little redundant since I've already disallowed unapproved collaborators, but better safe than sorry.

That's it. Works like a charm. Every day at noon, it automatically backs up my blog for free.

This approach should be adaptable for backing up almost any kind of website.

-

How might this fail?

Whenever I have to make choices that involve trade-offs, I like to document how my decisions might betray me down the line.

+

How might this fail?

Whenever I have to make choices that involve trade-offs, I like to document how my decisions might betray me down the line.

First and foremost, I'm completely at Herman's mercy here. He may choose to change how Bear Blog works in ways that will make my script fail. If he changes how analytics, upvotes, or image storage works, it may break my backups in ways I won't immediately notice.

Second, I'm also at the mercy of Microsoft since I'm relying on GitHub. In the unlikely event that GitHub completely disappears one day, my backups are gone. Microsoft could also choose to fundamentally change how their Actions product works, or what versions of software packages are available on the platform.

Microsoft could also suddenly decide my use of GitHub as a backup storage service runs afoul of their T&C. Seems unlikely. I doubt I'm costing them an entire cent. But it's technically possible.

I find these risks acceptable for my use case.

-
- - - - - + + + + + + + + + + + + + + +
+ -

- + + + + +
+ \ No newline at end of file diff --git a/jaysherby.com/block-users-in-auth0-from-the-command-line/index.html b/jaysherby.com/block-users-in-auth0-from-the-command-line/index.html index 8091424..a37b447 100644 --- a/jaysherby.com/block-users-in-auth0-from-the-command-line/index.html +++ b/jaysherby.com/block-users-in-auth0-from-the-command-line/index.html @@ -1,17 +1,26 @@ + - - -Block Users In Auth0 From The Command Line | Jay's blog - - + + + + + + Block Users In Auth0 From The Command Line | Jay's blog + + + + + + + @@ -21,6 +30,8 @@ As , I use Auth0 at work. Whenever a client decides not to renew their contract, which I'm thankful is an uncommon event[^1], I have to collect each user..."> + + @@ -29,6 +40,9 @@ As , I use Auth0 at work. Whenever a client decides not to renew their contract, which I'm thankful is an uncommon event[^1], I have to collect each user..."> + + + - - - - + + + -
- -

-Jay's blog -

-
- -
-
-

Block Users In Auth0 From The Command Line

-

- - - -

-

Original art, one of the Blockheads from Gumby

-

As previously established, I use Auth0 at work. Whenever a client decides not to renew their contract, which I'm thankful is an uncommon event1, I have to collect each user's email address from our application, search for their account in the Auth0 web app, and block each user.2 It's a tedious process.

+ +
+ +

+ Jay's blog +

+
+ +
+
+ + + + + + + +

Block Users In Auth0 From The Command Line

+ +

+ + + +

+ + +

Original art, one of the Blockheads from Gumby

+

As previously established, I use Auth0 at work. Whenever a client decides not to renew their contract, which I'm thankful is an uncommon event1, I have to collect each user's email address from our application, search for their account in the Auth0 web app, and block each user.2 It's a tedious process.

I've only had to do this process once so far. But being a developer, I immediately wanted to automate this process with a shell script. With Auth0's web app, it takes a copy/paste and 4 clicks per user I'm blocking. That's unacceptable. After the first half-dozen users, my eyes lose focus and my mind starts to wander. And that's when the odds I'll make a mistake spike.

-

This was the push I needed to finally install Auth0's command line tool, auth0-cli.

+

This was the push I needed to finally install Auth0's command line tool, auth0-cli.

After authenticating with auth0 login, the first step will be finding the user's ID based on their email address.

auth0 users search -q email:"user1@example.com"
 
-

Most commands in auth0-cli have a --json option to print their output in JSON format, which will make it easy to grab what I need by piping the output to jq.

+

Most commands in auth0-cli have a --json option to print their output in JSON format, which will make it easy to grab what I need by piping the output to jq.

With some error handling in case I receive anything besides exactly one result from my query, I'll have the user's ID. Using that, I should be able to block the user by... uh....

Let's see.... I can delete users using auth0 users delete, but as I mentioned before, that's not application in my case. I can "view" blocks3 on particular users via auth0 users blocks list, and I can unblock users with auth0 users blocks unblock.

Wait, what!? There doesn't seem to be any way to block a user via auth0-cli. How can that be? Isn't this one of the most common use cases for the tool?

-

If auth0-cli isn't up to the task, what are my options? I could script my browser, but that's awfully heavy-handed for a service that has an API. I could use the official Auth0 management API either via curl or a package in my favorite programming language. But this is actually really inconvenient compared to using auth0-cli, specifically because of authentication.

+

If auth0-cli isn't up to the task, what are my options? I could script my browser, but that's awfully heavy-handed for a service that has an API. I could use the official Auth0 management API either via curl or a package in my favorite programming language. But this is actually really inconvenient compared to using auth0-cli, specifically because of authentication.

With auth0-cli, I'm authenticated via my personal Auth0 management user account. It's no harder and no different from signing into the Auth0 management web app. If I wanted to use the Auth0 management API via other means, I'd have to use an ID/secret pair from an "application" (in Auth0's parlance). I could use the production creds our app uses, but that would be horrendous opsec.

I could create a temporary "application" to run this script and then delete it when I'm done. That's pretty inconvenient, though. If my shell script turns out well, I'd like to be able to share it with my coworkers in case they ever need to perform this kind of task.

If I felt like building a Rube Goldberg machine, maybe I could script the creation of a temporary "application" using auth0-cli and delete it when the script is done....

Ugh. Feels Bad, Man™. I ended up just doing the task manually. It was going to take more time to automate this than to do it by hand.4

-

Afterwards, I made a feature request for the ability to block users using the command line tool. Ewan Harris, a developer at Auth0, left a very helpful reply that pointed out a feature of the command line tool that I completely overlooked: auth0 api.

+

Afterwards, I made a feature request for the ability to block users using the command line tool. Ewan Harris, a developer at Auth0, left a very helpful reply that pointed out a feature of the command line tool that I completely overlooked: auth0 api.

The auth0 api command is essentially a passthrough that lets you call the management REST API directly using the credentials auth0-cli already has, contrasting the limited porcelain the tool provides. In other words, it's exactly what I was looking for! According to Ewan Harris, this would do the trick: auth0 api patch "users/<user_id>" --data "{\"blocked\":true}".

This is usually where I'd share a neat little shell script you can use to do this task. However, I haven't had occasion to need it. If that day comes, I'll write a shell script that takes a list of email addresses and blocks their user accounts. I'll try to share it here. In the meantime, you should have all the tools you need to do it yourself using auth0-cli and jq.

-
+
  1. Hi, HR! Thanks for reading! ✌️😜

  2. This is for the "why don't you just..." crowd. This is about a proprietary application owned by my employer, so I can't go into detail. But trust me that I can't delete the Auth0 accounts, and there's no in-application flag I can set to block a user at the moment.

  3. TIL there's more to blocking users in Auth0 than blocking them by hand like I'm doing. Apparently there are Auth0 features for automatically blocking new users who exhibit bot-like behavior. This was news to me as my employer's application is a fairly niche web app for medical clinics. We're not seeing nearly enough traffic to be subject to scripted sign-ups.

  4. -
  5. https://xkcd.com/1205/

  6. +
  7. https://xkcd.com/1205/

-

-#auth0 -

-
- - - - - + + + + + + +

+ + #auth0 + +

+ + + + + + + + + +
+ -
- + + + + +
+ \ No newline at end of file diff --git a/jaysherby.com/blog/index.html b/jaysherby.com/blog/index.html index 1ff34e8..a887722 100644 --- a/jaysherby.com/blog/index.html +++ b/jaysherby.com/blog/index.html @@ -1,28 +1,42 @@ + - - -Blog | Jay's blog - + + + + + + Blog | Jay's blog + + + + + - + + - + + + - + + + + - - + + + -
- -

-Jay's blog -

-
- -
-
+ +
+ +

+ Jay's blog +

+
+ +
+
+ + + + + + + + - + -
-
+ + + +
+ + + \ No newline at end of file diff --git a/jaysherby.com/blog/index.html?q=MacOS.html b/jaysherby.com/blog/index.html?q=MacOS.html index 8294636..94bab47 100644 --- a/jaysherby.com/blog/index.html?q=MacOS.html +++ b/jaysherby.com/blog/index.html?q=MacOS.html @@ -1,28 +1,42 @@ + - - -Blog | Jay's blog - + + + + + + Blog | Jay's blog + + + + + - + + - + + + - + + + + - - + + + -
- -

-Jay's blog -

-
- -
-
+ +
+ +

+ Jay's blog +

+
+ +
+
+ + +

Filtering for "MacOS"

Remove filter + + + + -
-
+ + + + +
+ + + \ No newline at end of file diff --git a/jaysherby.com/blog/index.html?q=auth0.html b/jaysherby.com/blog/index.html?q=auth0.html index 3fd1f41..f12b19e 100644 --- a/jaysherby.com/blog/index.html?q=auth0.html +++ b/jaysherby.com/blog/index.html?q=auth0.html @@ -1,28 +1,42 @@ + - - -Blog | Jay's blog - + + + + + + Blog | Jay's blog + + + + + - + + - + + + - + + + + - - + + + -
- -

-Jay's blog -

-
- -
-
+ +
+ +

+ Jay's blog +

+
+ +
+
+ + +

Filtering for "auth0"

Remove filter + + + + -
-
+ + + + +
+ + + \ No newline at end of file diff --git a/jaysherby.com/blog/index.html?q=docker.html b/jaysherby.com/blog/index.html?q=docker.html index 39aa530..c8d7b77 100644 --- a/jaysherby.com/blog/index.html?q=docker.html +++ b/jaysherby.com/blog/index.html?q=docker.html @@ -1,28 +1,42 @@ + - - -Blog | Jay's blog - + + + + + + Blog | Jay's blog + + + + + - + + - + + + - + + + + - - + + + -
- -

-Jay's blog -

-
- -
-
+ +
+ +

+ Jay's blog +

+
+ +
+
+ + +

Filtering for "docker"

Remove filter + + + + -
-
+ + + + +
+ + + \ No newline at end of file diff --git a/jaysherby.com/blog/index.html?q=electronics.html b/jaysherby.com/blog/index.html?q=electronics.html index 285a2e8..682ab8f 100644 --- a/jaysherby.com/blog/index.html?q=electronics.html +++ b/jaysherby.com/blog/index.html?q=electronics.html @@ -1,28 +1,42 @@ + - - -Blog | Jay's blog - + + + + + + Blog | Jay's blog + + + + + - + + - + + + - + + + + - - + + + -
- -

-Jay's blog -

-
- -
-
+ +
+ +

+ Jay's blog +

+
+ +
+
+ + +

Filtering for "electronics"

Remove filter + + + + -
-
+ + + + +
+ + + \ No newline at end of file diff --git a/jaysherby.com/blog/index.html?q=home server.html b/jaysherby.com/blog/index.html?q=home server.html index 4177162..6c72b02 100644 --- a/jaysherby.com/blog/index.html?q=home server.html +++ b/jaysherby.com/blog/index.html?q=home server.html @@ -1,28 +1,42 @@ + - - -Blog | Jay's blog - + + + + + + Blog | Jay's blog + + + + + - + + - + + + - + + + + - - + + + -
- -

-Jay's blog -

-
- -
-
+ +
+ +

+ Jay's blog +

+
+ +
+
+ + +

Filtering for "home server"

Remove filter + + + + -
-
+ + + + +
+ + + \ No newline at end of file diff --git a/jaysherby.com/blog/index.html?q=homebrew.html b/jaysherby.com/blog/index.html?q=homebrew.html index d8cac1a..d16bb9e 100644 --- a/jaysherby.com/blog/index.html?q=homebrew.html +++ b/jaysherby.com/blog/index.html?q=homebrew.html @@ -1,28 +1,42 @@ + - - -Blog | Jay's blog - + + + + + + Blog | Jay's blog + + + + + - + + - + + + - + + + + - - + + + -
- -

-Jay's blog -

-
- -
-
+ +
+ +

+ Jay's blog +

+
+ +
+
+ + +

Filtering for "homebrew"

Remove filter + + + + -
-
+ + + + +
+ + + \ No newline at end of file diff --git a/jaysherby.com/blog/index.html?q=javascript.html b/jaysherby.com/blog/index.html?q=javascript.html index 62e10c4..e9becae 100644 --- a/jaysherby.com/blog/index.html?q=javascript.html +++ b/jaysherby.com/blog/index.html?q=javascript.html @@ -1,28 +1,42 @@ + - - -Blog | Jay's blog - + + + + + + Blog | Jay's blog + + + + + - + + - + + + - + + + + - - + + + -
- -

-Jay's blog -

-
- -
-
+ +
+ +

+ Jay's blog +

+
+ +
+
+ + +

Filtering for "javascript"

Remove filter + + + + -
-
+ + + + +
+ + + \ No newline at end of file diff --git a/jaysherby.com/blog/index.html?q=linux.html b/jaysherby.com/blog/index.html?q=linux.html index ff82a74..0d73225 100644 --- a/jaysherby.com/blog/index.html?q=linux.html +++ b/jaysherby.com/blog/index.html?q=linux.html @@ -1,28 +1,42 @@ + - - -Blog | Jay's blog - + + + + + + Blog | Jay's blog + + + + + - + + - + + + - + + + + - - + + + -
- -

-Jay's blog -

-
- -
-
+ +
+ +

+ Jay's blog +

+
+ +
+
+ + +

Filtering for "linux"

Remove filter + + + + -
-
+ + + + +
+ + + \ No newline at end of file diff --git a/jaysherby.com/blog/index.html?q=nodejs.html b/jaysherby.com/blog/index.html?q=nodejs.html index 9c99648..445204a 100644 --- a/jaysherby.com/blog/index.html?q=nodejs.html +++ b/jaysherby.com/blog/index.html?q=nodejs.html @@ -1,28 +1,42 @@ + - - -Blog | Jay's blog - + + + + + + Blog | Jay's blog + + + + + - + + - + + + - + + + + - - + + + -
- -

-Jay's blog -

-
- -
-
+ +
+ +

+ Jay's blog +

+
+ +
+
+ + +

Filtering for "nodejs"

Remove filter + + + + -
-
+ + + + +
+ + + \ No newline at end of file diff --git a/jaysherby.com/blog/index.html?q=podman.html b/jaysherby.com/blog/index.html?q=podman.html index ab2b354..db6fa72 100644 --- a/jaysherby.com/blog/index.html?q=podman.html +++ b/jaysherby.com/blog/index.html?q=podman.html @@ -1,28 +1,42 @@ + - - -Blog | Jay's blog - + + + + + + Blog | Jay's blog + + + + + - + + - + + + - + + + + - - + + + -
- -

-Jay's blog -

-
- -
-
+ +
+ +

+ Jay's blog +

+
+ +
+
+ + +

Filtering for "podman"

Remove filter + + + + -
-
+ + + + +
+ + + \ No newline at end of file diff --git a/jaysherby.com/blog/index.html?q=pop_os.html b/jaysherby.com/blog/index.html?q=pop_os.html index 0d10b8b..275fc39 100644 --- a/jaysherby.com/blog/index.html?q=pop_os.html +++ b/jaysherby.com/blog/index.html?q=pop_os.html @@ -1,28 +1,42 @@ + - - -Blog | Jay's blog - + + + + + + Blog | Jay's blog + + + + + - + + - + + + - + + + + - - + + + -
- -

-Jay's blog -

-
- -
-
+ +
+ +

+ Jay's blog +

+
+ +
+
+ + +

Filtering for "pop_os"

Remove filter + + + + -
-
+ + + + +
+ + + \ No newline at end of file diff --git a/jaysherby.com/blog/index.html?q=postgres.html b/jaysherby.com/blog/index.html?q=postgres.html index 1bdb9ea..53bce1c 100644 --- a/jaysherby.com/blog/index.html?q=postgres.html +++ b/jaysherby.com/blog/index.html?q=postgres.html @@ -1,28 +1,42 @@ + - - -Blog | Jay's blog - + + + + + + Blog | Jay's blog + + + + + - + + - + + + - + + + + - - + + + -
- -

-Jay's blog -

-
- -
-
+ +
+ +

+ Jay's blog +

+
+ +
+
+ + +

Filtering for "postgres"

Remove filter + + + + -
-
+ + + + +
+ + + \ No newline at end of file diff --git a/jaysherby.com/blog/index.html?q=programming language theory.html b/jaysherby.com/blog/index.html?q=programming language theory.html index 886011e..4cd3f4c 100644 --- a/jaysherby.com/blog/index.html?q=programming language theory.html +++ b/jaysherby.com/blog/index.html?q=programming language theory.html @@ -1,28 +1,42 @@ + - - -Blog | Jay's blog - + + + + + + Blog | Jay's blog + + + + + - + + - + + + - + + + + - - + + + -
- -

-Jay's blog -

-
- -
-
+ +
+ +

+ Jay's blog +

+
+ +
+
+ + +

Filtering for "programming language theory"

Remove filter + + + + -
-
+ + + + +
+ + + \ No newline at end of file diff --git a/jaysherby.com/blog/index.html?q=react.html b/jaysherby.com/blog/index.html?q=react.html index ae47544..8ed4f4a 100644 --- a/jaysherby.com/blog/index.html?q=react.html +++ b/jaysherby.com/blog/index.html?q=react.html @@ -1,28 +1,42 @@ + - - -Blog | Jay's blog - + + + + + + Blog | Jay's blog + + + + + - + + - + + + - + + + + - - + + + -
- -

-Jay's blog -

-
- -
-
+ +
+ +

+ Jay's blog +

+
+ +
+
+ + +

Filtering for "react"

Remove filter + + + + -
-
+ + + + +
+ + + \ No newline at end of file diff --git a/jaysherby.com/blog/index.html?q=sequelize.html b/jaysherby.com/blog/index.html?q=sequelize.html index c5ee63f..d770693 100644 --- a/jaysherby.com/blog/index.html?q=sequelize.html +++ b/jaysherby.com/blog/index.html?q=sequelize.html @@ -1,28 +1,42 @@ + - - -Blog | Jay's blog - + + + + + + Blog | Jay's blog + + + + + - + + - + + + - + + + + - - + + + -
- -

-Jay's blog -

-
- -
-
+ +
+ +

+ Jay's blog +

+
+ +
+
+ + +

Filtering for "sequelize"

Remove filter + + + + -
-
+ + + + +
+ + + \ No newline at end of file diff --git a/jaysherby.com/blog/index.html?q=soldering.html b/jaysherby.com/blog/index.html?q=soldering.html index e151e2f..3a510fd 100644 --- a/jaysherby.com/blog/index.html?q=soldering.html +++ b/jaysherby.com/blog/index.html?q=soldering.html @@ -1,28 +1,42 @@ + - - -Blog | Jay's blog - + + + + + + Blog | Jay's blog + + + + + - + + - + + + - + + + + - - + + + -
- -

-Jay's blog -

-
- -
-
+ +
+ +

+ Jay's blog +

+
+ +
+
+ + +

Filtering for "soldering"

Remove filter + + + + -
-
+ + + + +
+ + + \ No newline at end of file diff --git a/jaysherby.com/blog/index.html?q=sql.html b/jaysherby.com/blog/index.html?q=sql.html index 7615981..8d3c06a 100644 --- a/jaysherby.com/blog/index.html?q=sql.html +++ b/jaysherby.com/blog/index.html?q=sql.html @@ -1,28 +1,42 @@ + - - -Blog | Jay's blog - + + + + + + Blog | Jay's blog + + + + + - + + - + + + - + + + + - - + + + -
- -

-Jay's blog -

-
- -
-
+ +
+ +

+ Jay's blog +

+
+ +
+
+ + +

Filtering for "sql"

Remove filter + + + + -
-
+ + + + +
+ + + \ No newline at end of file diff --git a/jaysherby.com/blog/index.html?q=systemd.html b/jaysherby.com/blog/index.html?q=systemd.html index 5a145fc..5332903 100644 --- a/jaysherby.com/blog/index.html?q=systemd.html +++ b/jaysherby.com/blog/index.html?q=systemd.html @@ -1,28 +1,42 @@ + - - -Blog | Jay's blog - + + + + + + Blog | Jay's blog + + + + + - + + - + + + - + + + + - - + + + -
- -

-Jay's blog -

-
- -
-
+ +
+ +

+ Jay's blog +

+
+ +
+
+ + +

Filtering for "systemd"

Remove filter + + + + -
-
+ + + + +
+ + + \ No newline at end of file diff --git a/jaysherby.com/blog/index.html?q=terrorform.html b/jaysherby.com/blog/index.html?q=terrorform.html index c4605f2..5f4e5f6 100644 --- a/jaysherby.com/blog/index.html?q=terrorform.html +++ b/jaysherby.com/blog/index.html?q=terrorform.html @@ -1,28 +1,42 @@ + - - -Blog | Jay's blog - + + + + + + Blog | Jay's blog + + + + + - + + - + + + - + + + + - - + + + -
- -

-Jay's blog -

-
- -
-
+ +
+ +

+ Jay's blog +

+
+ +
+
+ + +

Filtering for "terrorform"

Remove filter + + + + -
-
+ + + + +
+ + + \ No newline at end of file diff --git a/jaysherby.com/blog/index.html?q=til.html b/jaysherby.com/blog/index.html?q=til.html index 98307ba..ebbcbd2 100644 --- a/jaysherby.com/blog/index.html?q=til.html +++ b/jaysherby.com/blog/index.html?q=til.html @@ -1,28 +1,42 @@ + - - -Blog | Jay's blog - + + + + + + Blog | Jay's blog + + + + + - + + - + + + - + + + + - - + + + -
- -

-Jay's blog -

-
- -
-
+ +
+ +

+ Jay's blog +

+
+ +
+
+ + +

Filtering for "til"

Remove filter + + + + -
-
+ + + + +
+ + + \ No newline at end of file diff --git a/jaysherby.com/blog/index.html?q=tools.html b/jaysherby.com/blog/index.html?q=tools.html index 595ea76..1fe5676 100644 --- a/jaysherby.com/blog/index.html?q=tools.html +++ b/jaysherby.com/blog/index.html?q=tools.html @@ -1,28 +1,42 @@ + - - -Blog | Jay's blog - + + + + + + Blog | Jay's blog + + + + + - + + - + + + - + + + + - - + + + -
- -

-Jay's blog -

-
- -
-
+ +
+ +

+ Jay's blog +

+
+ +
+
+ + +

Filtering for "tools"

Remove filter + + + + -
-
+ + + + +
+ + + \ No newline at end of file diff --git a/jaysherby.com/blog/index.html?q=typescript.html b/jaysherby.com/blog/index.html?q=typescript.html index dfe769a..d8bd414 100644 --- a/jaysherby.com/blog/index.html?q=typescript.html +++ b/jaysherby.com/blog/index.html?q=typescript.html @@ -1,28 +1,42 @@ + - - -Blog | Jay's blog - + + + + + + Blog | Jay's blog + + + + + - + + - + + + - + + + + - - + + + -
- -

-Jay's blog -

-
- -
-
+ +
+ +

+ Jay's blog +

+
+ +
+
+ + +

Filtering for "typescript"

Remove filter + + + + -
-
+ + + + +
+ + + \ No newline at end of file diff --git a/jaysherby.com/expresss-finish-event/index.html b/jaysherby.com/expresss-finish-event/index.html index d605872..b6ae344 100644 --- a/jaysherby.com/expresss-finish-event/index.html +++ b/jaysherby.com/expresss-finish-event/index.html @@ -1,15 +1,24 @@ + - - -Express's "finish" Event | Jay's blog - - + + + + + + Express's "finish" Event | Jay's blog + + + + + + + @@ -17,12 +26,17 @@ + + + + + - - - - + + + -
- -

-Jay's blog -

-
- -
-
-

Express's "finish" Event

-

- - - -

-

Let's say you want to write some middleware for Express that does something after the request is complete. The standard advice on the web is to add an event listener to the response object listening for the "finish" event.

+ +
+ +

+ Jay's blog +

+
+ +
+
+ + + + + + + +

Express's "finish" Event

+ +

+ + + +

+ + +

Let's say you want to write some middleware for Express that does something after the request is complete. The standard advice on the web is to add an event listener to the response object listening for the "finish" event.

app.use(function(req, res, next) {
   // DON'T USE THIS!!!
   res.on('finish', function() {
@@ -326,7 +371,7 @@ 

Express's "finish" Event

next(); });
-

I learned the hard way that the "finish" event doesn't always get fired. There are lots of ways you can screw this up as a programmer. "finish" may not fire if:

+

I learned the hard way that the "finish" event doesn't always get fired. There are lots of ways you can screw this up as a programmer. "finish" may not fire if:

  • there's a break in the middleware chain, i.e. not calling next
  • the response isn't completed using res.send or res.end
  • @@ -341,22 +386,39 @@

    Express's "finish" Event

    next(); }); -

    -#nodejs -

    -
    - - - - - + + + + + + +

    + + #nodejs + +

    + + + + + + + + + +
    + -
- + + + + +
+ \ No newline at end of file diff --git a/jaysherby.com/homebrew-shell-completion-is-worth-your-time/index.html b/jaysherby.com/homebrew-shell-completion-is-worth-your-time/index.html index d56e5cf..7bc4d5f 100644 --- a/jaysherby.com/homebrew-shell-completion-is-worth-your-time/index.html +++ b/jaysherby.com/homebrew-shell-completion-is-worth-your-time/index.html @@ -1,17 +1,26 @@ + - - -Homebrew Shell Completion Is Worth Your Time | Jay's blog - - + + + + + + Homebrew Shell Completion Is Worth Your Time | Jay's blog + + + + + + + @@ -21,6 +30,8 @@ I finally gave the Auth0 command line tool a try yesterday at work. I installed it via Homebrew since I use a MacBook Pro at work. I was hoping I could u..."> + + @@ -29,6 +40,9 @@ I finally gave the Auth0 command line tool a try yesterday at work. I installed it via Homebrew since I use a MacBook Pro at work. I was hoping I could u..."> + + + - - - + + + -
- -

-Jay's blog -

-
- -
-
-

Homebrew Shell Completion Is Worth Your Time

-

- - - -

-

Original art, nautilus shell

+ +
+ +

+ Jay's blog +

+
+ +
+
+ + + + + + + +

Homebrew Shell Completion Is Worth Your Time

+ +

+ + + +

+ + +

Original art, nautilus shell

I finally gave the Auth0 command line tool a try yesterday at work. I installed it via Homebrew since I use a MacBook Pro at work. I was hoping I could use it to quickly automate a repetitive task using a shell script. Unfortunately, Auth0's tool isn't currently able to block user accounts.1

I noticed that their tool had a way to output a shell completion configuration. That led me down a rabbit hole, revealing that lots of tools I've installed via Homebrew ship with shell completions! They just aren't being used by default.

-

I'm not going to repeat the instructions here because Homebrew has a webpage with concise yet detailed instructions for adding shell completions to your configuration.

+

I'm not going to repeat the instructions here because Homebrew has a webpage with concise yet detailed instructions for adding shell completions to your configuration.

It's worth your time to set this up. Shell completions are a super power. I'm much less productive if I don't have git shell completions available.

I will note that setting this up for zsh, now the default shell in MacOS, is the most involved of all the supported shells. It's worth the 5 minute investment, though.

-
+
    -
  1. I opened a feature ticket.

  2. +
  3. I opened a feature ticket.

-

-#MacOS -#homebrew -

-
- - - - - + + + + + + +

+ + #MacOS + + #homebrew + +

+ + + + + + + + + +
+ -
- + + + + +
+ \ No newline at end of file diff --git a/jaysherby.com/how-i-fixed-laggy-wifi-in-debian-12/index.html b/jaysherby.com/how-i-fixed-laggy-wifi-in-debian-12/index.html index c443c2b..f2209a2 100644 --- a/jaysherby.com/how-i-fixed-laggy-wifi-in-debian-12/index.html +++ b/jaysherby.com/how-i-fixed-laggy-wifi-in-debian-12/index.html @@ -1,15 +1,24 @@ + - - -How I Fixed Laggy Wifi In Debian 12 | Jay's blog - - + + + + + + How I Fixed Laggy Wifi In Debian 12 | Jay's blog + + + + + + + @@ -17,12 +26,17 @@ + + + + + - - - - + + + -
- -

-Jay's blog -

-
- -
-
-

How I Fixed Laggy Wifi In Debian 12

-

- - - -

-

The time has finally come to replace my Raspberry Pi 4 home server with something a little more capable. It's been dutifully chugging along for more than 4 years, but my needs finally exceed its capabilities.

-

I got a good deal on a Lenovo ThinkCentre M910q from Back Market. When I went to install Debian 12 Bookworm, I found that Debian's non-free firmware packages don't include the necessary drivers for the cheap USB wifi adapter the seller included. Rather than faff about trying to compile and install the kernel module, I bought an Intel 8265 M.2 wifi adapter from Amazon. Its drivers are included in Debian's non-free firmware, and according to Lenovo's service manuals, this was the model they would have included in my machine had that option been chosen during configuration.

+ +
+ +

+ Jay's blog +

+
+ +
+
+ + + + + + + +

How I Fixed Laggy Wifi In Debian 12

+ +

+ + + +

+ + +

The time has finally come to replace my Raspberry Pi 4 home server with something a little more capable. It's been dutifully chugging along for more than 4 years, but my needs finally exceed its capabilities.

+

I got a good deal on a Lenovo ThinkCentre M910q from Back Market. When I went to install Debian 12 Bookworm, I found that Debian's non-free firmware packages don't include the necessary drivers for the cheap USB wifi adapter the seller included. Rather than faff about trying to compile and install the kernel module, I bought an Intel 8265 M.2 wifi adapter from Amazon. Its drivers are included in Debian's non-free firmware, and according to Lenovo's service manuals, this was the model they would have included in my machine had that option been chosen during configuration.

Installation of Debian worked without any issues. However, once the system was running, I noticed SSH sessions were very laggy. Pings to the new machine were averaging around 150 milliseconds, which is ten times longer than pings to the Raspberry Pi it was set to replace.

After a little searching online, I found the culprit to be the power management on the wifi adapter. I think the wifi adapter goes into a low-power state after a lull in network traffic, but the lull is so short that the time between key presses in a SSH session is enough to trigger it. Because this device is not running on a battery, I feel comfortable turning the wifi adapter's power management off. This appears the solve the issue, and my SSH sessions are properly peppy afterwards.

server-user@m910q:~$ sudo iw dev wlp1s0 set power_save off
@@ -340,24 +385,43 @@ 

How I Fixed Laggy Wifi In Debian 12

sudo systemctl enable --now wlan_always_on@wlp1s0.service
 

This setup may not be perfect. I'm no systemd expect. This is cobbled and cargo culted together. But it works on my machine™.

-

-#home server -#linux -#systemd -

-
- - - - - + + + + + + +

+ + #home server + + #linux + + #systemd + +

+ + + + + + + + + +
+ -
- + + + + +
+ \ No newline at end of file diff --git a/jaysherby.com/hp-dev-one-low-disk-space-on-bootefi/index.html b/jaysherby.com/hp-dev-one-low-disk-space-on-bootefi/index.html index 133489f..7de58b3 100644 --- a/jaysherby.com/hp-dev-one-low-disk-space-on-bootefi/index.html +++ b/jaysherby.com/hp-dev-one-low-disk-space-on-bootefi/index.html @@ -1,15 +1,24 @@ + - - -HP Dev One, low disk space on /boot/efi | Jay's blog - - + + + + + + HP Dev One, low disk space on /boot/efi | Jay's blog + + + + + + + @@ -17,12 +26,17 @@ + + + + + - - - + + + -
- -

-Jay's blog -

-
- -
-
-

HP Dev One, low disk space on /boot/efi

-

- - - -

-

I always seem to back the wrong horse when it comes to laptops. Long story short, I was one of the suckers who bought the HP Dev One. Don't get me wrong, I'm generally very happy with it. But HP discontinued the line and my daily driver laptop is a technological dead end.

-

Recently I started receiving warning messages that my boot partition, /boot/efi is low on space. Sure enough, the 512 MB boot partition is 96% full.

-

I searched the internet and while there are plenty of people complaining about this issue, there seem to be few fixes. Many recommend increasing the size of the partition to 1 GB, though they admit it's a complicated process to do so. The easiest way is a full reinstall of the OS, although you can technically resize the partitions without such a drastic course of action if you're willing to futz around.

+ +
+ +

+ Jay's blog +

+
+ +
+
+ + + + + + + +

HP Dev One, low disk space on /boot/efi

+ +

+ + + +

+ + +

I always seem to back the wrong horse when it comes to laptops. Long story short, I was one of the suckers who bought the HP Dev One. Don't get me wrong, I'm generally very happy with it. But HP discontinued the line and my daily driver laptop is a technological dead end.

+

Recently I started receiving warning messages that my boot partition, /boot/efi is low on space. Sure enough, the 512 MB boot partition is 96% full.

+

I searched the internet and while there are plenty of people complaining about this issue, there seem to be few fixes. Many recommend increasing the size of the partition to 1 GB, though they admit it's a complicated process to do so. The easiest way is a full reinstall of the OS, although you can technically resize the partitions without such a drastic course of action if you're willing to futz around.

I tried doing a "system refresh" via means built into Pop!_OS. I didn't lose any of my personal data, but I did have to reinstall all my applications. It seemed to fix the issue, although boot partition usage still hovered on the brink at 95%.

-

That fix lasted all of a week. There was another kernel update today causing usage to hit 97%. The warning returned.

-

I did some more searching today and found this GitHub thread. For posterity, if you're using an HP Dev One laptop like I am, there's a high likelihood that an old, already installed firmware update is hanging around in the boot partition, taking up precious space.

+

That fix lasted all of a week. There was another kernel update today causing usage to hit 97%. The warning returned.

+

I did some more searching today and found this GitHub thread. For posterity, if you're using an HP Dev One laptop like I am, there's a high likelihood that an old, already installed firmware update is hanging around in the boot partition, taking up precious space.

It's safe to delete /boot/efi/EFI/HP and /boot/efi/EFI/pop directories, which hold firmware update files.

After deleting those directories, my boot partition is now down to a relatively safe 82% usage.

-

-#pop_os -

-
- - - - - + + + + + + +

+ + #pop_os + +

+ + + + + + + + + +
+ -
- + + + + +
+ \ No newline at end of file diff --git a/jaysherby.com/index.html b/jaysherby.com/index.html index 24cfe61..6d594a6 100644 --- a/jaysherby.com/index.html +++ b/jaysherby.com/index.html @@ -1,12 +1,20 @@ + - - -Jay's blog - + + + + + + Jay's blog + + + + + + @@ -29,6 +38,8 @@ Hiring? Check to see..."> + + @@ -41,6 +52,9 @@ Hiring? Check to see..."> + + + - - + + + -
- -

-Jay's blog -

-
- -
-
+ +
+ +

+ Jay's blog +

+
+ +
+
+ + + +

-Hi! I'm Jay Sherby! + Hi! I'm Jay Sherby!

I'm an experienced software developer in Chicago.

-

Hiring? Check my résumé to see if we're a match.

-
-
+

Hiring? Check my résumé to see if we're a match.

+ + + + + + + +
+ + + \ No newline at end of file diff --git a/jaysherby.com/nullable-parameters-in-sql-queries/index.html b/jaysherby.com/nullable-parameters-in-sql-queries/index.html index daf53fb..cfa2793 100644 --- a/jaysherby.com/nullable-parameters-in-sql-queries/index.html +++ b/jaysherby.com/nullable-parameters-in-sql-queries/index.html @@ -1,12 +1,20 @@ + - - -Nullable Parameters in SQL Queries | Jay's blog - - + + + + + + Nullable Parameters in SQL Queries | Jay's blog + + + + + + + @@ -25,6 +34,8 @@ That ..."> + + @@ -35,6 +46,9 @@ That ..."> + + + - - - - + + + -
- -

-Jay's blog -

-
- -
-
-

Nullable Parameters in SQL Queries

-

- - - -

-

A common beginners' trap in SQL is the NULL value. Nothing equals NULL, not even itself. Imagine a table called foo with a nullable column called bar.

+ +
+ +

+ Jay's blog +

+
+ +
+
+ + + + + + + +

Nullable Parameters in SQL Queries

+ +

+ + + +

+ + +

A common beginners' trap in SQL is the NULL value. Nothing equals NULL, not even itself. Imagine a table called foo with a nullable column called bar.

-- THIS IS A BROKEN QUERY!
 SELECT * FROM foo WHERE bar = NULL;
 
@@ -345,7 +390,7 @@

Nullable Parameters in SQL Queries

SELECT * FROM foo WHERE bar IS NULL;
 

That's all well and good when you're writing custom queries by hand. Just remember to use IS NULL. But what about when you're writing a parameterized query and one of the parameters may be NULL, or it may be a value?

-

I've seen some tricks out there that use COALESCE. But if you're using PostgreSQL, you're in luck. There's a very nice, clean solution available: IS NOT DISTINCT FROM.

+

I've seen some tricks out there that use COALESCE. But if you're using PostgreSQL, you're in luck. There's a very nice, clean solution available: IS NOT DISTINCT FROM.

PREPARE my_query (integer) AS
 SELECT * FROM foo WHERE bar IS NOT DISTINCT FROM $1;
 
@@ -355,24 +400,43 @@ 

Nullable Parameters in SQL Queries

If you've never heard of the IS NOT DISTINCT FROM operator, neither had I until today. I'm glad it exists, but using the word "distinct" makes this handy operator very difficult to search for online.

For the sake of readability, I'd recommend only using this operator in this particular situation. In other words, don't rush to go change every usage of = and IS NULL in queries that don't need this special consideration. This is a special tool that deserves a spot in your tool belt for just the right occasion.

-

-#postgres -#sql -#til -

-
- - - - - + + + + + + +

+ + #postgres + + #sql + + #til + +

+ + + + + + + + + +
+ -
- + + + + +
+ \ No newline at end of file diff --git a/jaysherby.com/ode-to-a-desoldering-pump/index.html b/jaysherby.com/ode-to-a-desoldering-pump/index.html index 4282576..9160741 100644 --- a/jaysherby.com/ode-to-a-desoldering-pump/index.html +++ b/jaysherby.com/ode-to-a-desoldering-pump/index.html @@ -1,17 +1,26 @@ + - - -Ode To A Desoldering Pump | Jay's blog - - + + + + + + Ode To A Desoldering Pump | Jay's blog + + + + + + + @@ -21,6 +30,8 @@ The most perso..."> + + @@ -29,6 +40,9 @@ The most perso..."> + + + - - - + + + -
- -

-Jay's blog -

-
- -
-
-

Ode To A Desoldering Pump

-

- - - -

-

Sometimes I solder electronics. I take no joy in it. I do it as a means to an end. I consider myself an experienced beginner and hobbyist. I am not an expert.

+ +
+ +

+ Jay's blog +

+
+ +
+
+ + + + + + + +

Ode To A Desoldering Pump

+ +

+ + + +

+ + +

Sometimes I solder electronics. I take no joy in it. I do it as a means to an end. I consider myself an experienced beginner and hobbyist. I am not an expert.

The most personally frustrating part of soldering for me is correcting a mistake. I've soldered the wrong resistor into a spot on a really cramped board. I've already snipped the leads. Now what?

Previously, I'd curse myself a few times, break out the desoldering braid, and pray. Desoldering braid, in my experience, is terrible. I can count on one hand the number of occasions it worked without issue.

Desoldering Pump

@@ -335,30 +381,49 @@

Ode To A Desoldering Pump

While the desoldering pump is the biggest game-changing tool I've added to my hobby electronics arsenal, I want to share some other recommendations that have vastly improved my experience.

Most people know about "helping hands" tools that hold boards and components for you while you solder. The most common variations are a heavy chunk of metal with a bunch of ball joints with wing nuts to loosen and tighten the joint. They typically have two alligator clip "hands" and a magnifying glass. And they're garbage.

Omnifixo Helping Hands

-

Check out Omnifixo instead. It's designed by a guy from Sweden, and it's amazing. Go check out the website to see its genius design and all of the clever little features. I can attest that it exceeds all expectations I had for a helping hands tool. If you're used to paying under $10 for the cheap versions of this tool, you might experience a bit of sticker shock. But this is another tool that's well worth the price.

+

Check out Omnifixo instead. It's designed by a guy from Sweden, and it's amazing. Go check out the website to see its genius design and all of the clever little features. I can attest that it exceeds all expectations I had for a helping hands tool. If you're used to paying under $10 for the cheap versions of this tool, you might experience a bit of sticker shock. But this is another tool that's well worth the price.

Finally, what the hell is that component? I can't read the label on it. It's got a number printed on it. I searched it on the web and figured it out, but that took like 10 minutes. Now that I know what it is, how can I be sure it's even working?

T7 Multi-function Component Tester

There's a tool for that. Search your favorite marketplace that drop-ships cheap knock-offs from China (i.e. Amazon, eBay, AliExpress, etc.) for "LCR-T7" or "T7 Tester". You should be able to find a small device with a screen, a teal zero insertion force socket, and a single yellow button for around $20.

This thing is amazing! Plug in almost any passive component and it'll tell you what it is. It'll tell you the resistance of resistors, the capacitance of capacitors, and the direction of diodes. It'll identify the pins of transistors. And it does it all in mere seconds.

In case it wasn't obvious, I'm not making any commissions, kickbacks, or money of any kind by recommending these tools. The only one I linked to was the Omniflexo because you can only buy it from its website right now. There are no affiliate links or anything like that.

-

-#electronics -#soldering -#tools -

-
- - - - - + + + + + + +

+ + #electronics + + #soldering + + #tools + +

+ + + + + + + + + +
+ -
- + + + + +
+ \ No newline at end of file diff --git a/jaysherby.com/randomizing-mac-addresses-with-networkmanager/index.html b/jaysherby.com/randomizing-mac-addresses-with-networkmanager/index.html index 394a810..a425653 100644 --- a/jaysherby.com/randomizing-mac-addresses-with-networkmanager/index.html +++ b/jaysherby.com/randomizing-mac-addresses-with-networkmanager/index.html @@ -1,15 +1,24 @@ - - - -Randomizing My MAC Address With NetworkManager | Jay's blog - - + + + + + + + Randomizing My MAC Address With NetworkManager | Jay's blog + + + + + + + + @@ -17,12 +26,17 @@ + + + + + - - - - + + + -
- -

-Jay's blog -

-
- -
-
-

Randomizing My MAC Address With NetworkManager

-

- - - -

-

My life is harder for knowing and caring about my own privacy and security. For example, if you've ever connected to the wifi at a Starbucks using a laptop, you'll have noticed the "captive portal" system they use. That's the page that pops up before you can actually use the internet. It asks you for your name, email address, and zip code.

-

You may also notice that once you fill out that form, you never have to do it again. Every time you connect to the Starbucks wifi afterwards, it'll usually still give you a captive portal page, but you'll never have to "log in" again. This usually holds true even if you visit a different Starbucks with the same computer.

+ +
+ +

+ Jay's blog +

+
+ +
+
+ + + + + + + +

Randomizing My MAC Address With NetworkManager

+ +

+ + + +

+ + +

My life is harder for knowing and caring about my own privacy and security. For example, if you've ever connected to the wifi at a Starbucks using a laptop, you'll have noticed the "captive portal" system they use. That's the page that pops up before you can actually use the internet. It asks you for your name, email address, and zip code.

+

You may also notice that once you fill out that form, you never have to do it again. Every time you connect to the Starbucks wifi afterwards, it'll usually still give you a captive portal page, but you'll never have to "log in" again. This usually holds true even if you visit a different Starbucks with the same computer.

Guess what? Starbucks is tracking you.1 I don't like that, so I lie every time I fill out that form.2 Or do I? 😉

The way Starbucks remembers your computer is by recording the MAC address of your wifi adapter.

This is a very old, very well-known privacy risk. Most operating systems have made it relatively easy to spoof your MAC address with a semi-randomly generated one to mitigate this risk. Some even do this by default now.

Even so, I have particular preferences for how I prefer this feature to work that my system's default settings don't meet.

-

I'm using Pop!_OS, which uses NetworkManager. I'll show you its default setup, how I changed it, and why.

+

I'm using Pop!_OS, which uses NetworkManager. I'll show you its default setup, how I changed it, and why.

Here's the default config, located at /etc/NetworkManager/NetworkManager.conf, that shipped with my machine:

[main]
 plugins=ifupdown,keyfile
@@ -334,7 +379,7 @@ 

Randomizing My MAC Address With NetworkManager

[device] wifi.scan-rand-mac-address=no
-

Shame on you, Pop!_OS! See wifi.scan-rand-mac-address=no? That turns off MAC randomization during access point scanning! That means you're leaking your MAC address before you even connect to wifi. Passive listeners can track you. This isn't even the default behavior as of NetworkManager 1.4.0. This is pretty embarrassing. Even Android randomizes your MAC address when scanning nowadays.

+

Shame on you, Pop!_OS! See wifi.scan-rand-mac-address=no? That turns off MAC randomization during access point scanning! That means you're leaking your MAC address before you even connect to wifi. Passive listeners can track you. This isn't even the default behavior as of NetworkManager 1.4.0. This is pretty embarrassing. Even Android randomizes your MAC address when scanning nowadays.

Here are my changes:

[main]
 plugins=ifupdown,keyfile
@@ -354,39 +399,56 @@ 

Randomizing My MAC Address With NetworkManager

Beside explicitly specifying a MAC address, the special values "preserve", "permanent", "random" and "stable" are supported. "preserve" means not to touch the MAC address on activation. "permanent" means to use the permanent hardware address if the device has one (otherwise this is treated as "preserve"). "random" creates a random MAC address on each connect. "stable" creates a hashed MAC address based on connection.stable-id and a machine dependent key.

-

In other words, permanent and probably preserve will leak your physical MAC address when you connect to an access point. stable won't leak your physical MAC address, but it will result in a given access point seeing the same fake MAC address every time you connect to it. That doesn't really prevent tracking if every Starbucks access point presents identically.4 I want random. It generates a new, random MAC address on every connection.

+

In other words, permanent and probably preserve will leak your physical MAC address when you connect to an access point. stable won't leak your physical MAC address, but it will result in a given access point seeing the same fake MAC address every time you connect to it. That doesn't really prevent tracking if every Starbucks access point presents identically.4 I want random. It generates a new, random MAC address on every connection.

There. No more tracking. Now I just have to fill out that stupid captive portal form every time I go to Starbucks with my laptop.5

-

But what if my router yells at me?

This happened to me. My Synology router started sending push notifications to my phone every time I turned on my laptop, asking me if I know this mysterious device that just connected.

-

I needed to make an exception to the rules, but just for this one wifi network. Happily, the option to change this setting is actually in the GUI.

+

But what if my router yells at me?

This happened to me. My Synology router started sending push notifications to my phone every time I turned on my laptop, asking me if I know this mysterious device that just connected.

+

I needed to make an exception to the rules, but just for this one wifi network. Happily, the option to change this setting is actually in the GUI.

Oh, no! I forgot to censor my home network's SSID!

You can see here I've set this particular connection to stable. Works like a charm.

I'm sure there's a way to do this on the command line, or in a configuration file somewhere, but I haven't needed to figure that out yet.

-
+
  1. I have no idea if Starbucks is actively using the data they're collecting, but they are collecting it. Even if it's just within log files. I mean, they wouldn't ask you a bunch of personal information if they weren't going to use it for something, right? I'm sure they tell you what they're doing with it in their T&C. But who reads those?

  2. I've noticed that they'll deny you access if you make up random email domains. Gmail addresses seem to always work, though.

  3. My laptop doesn't have a built-in ethernet port, so this is probably unnecessary. But it's not hurting anything, and it'll protect me in the one-in-a-million chance I ever plug in a USB-to-ethernet dongle.

  4. And almost every Starbucks access point does present identically. This is why your phone or laptop will automatically connect to Starbucks wifi even if you've never been to that particular location before.

  5. -
  6. Yep. I'm apparently willing to pay that price because I care that much about my privacy. Do you think I like living this way? I guess my Walter Mitty daydreams include living in the world of Hackers (1995), and running Silk Road from coffee shops but not getting caught like Ulbricht did.

  7. +
  8. Yep. I'm apparently willing to pay that price because I care that much about my privacy. Do you think I like living this way? I guess my Walter Mitty daydreams include living in the world of Hackers (1995), and running Silk Road from coffee shops but not getting caught like Ulbricht did.

-

-#pop_os -

-
- - - - - + + + + + + +

+ + #pop_os + +

+ + + + + + + + + +
+ -
- + + + + +
+ \ No newline at end of file diff --git a/jaysherby.com/react-hook-form-a-leaky-abstraction/index.html b/jaysherby.com/react-hook-form-a-leaky-abstraction/index.html index 811d9f7..ba3db4e 100644 --- a/jaysherby.com/react-hook-form-a-leaky-abstraction/index.html +++ b/jaysherby.com/react-hook-form-a-leaky-abstraction/index.html @@ -1,15 +1,24 @@ - - - -React Hook Form: A Leaky Abstraction | Jay's blog - - + + + + + + + React Hook Form: A Leaky Abstraction | Jay's blog + + + + + + + + @@ -17,12 +26,17 @@ + + + + + - - - - + + + -
- -

-Jay's blog -

-
- -
-
-

React Hook Form: A Leaky Abstraction

-

- - - -

-

I have used a handful of helper libraries for wrangling form state in React over the course of my career. In my current job, we're using React Hook Form1, specifically v6, although I think my story might still apply to those using the newer v7 branch. I enjoy working with it for the most part. I've found it to be a lighter-weight alternative to Formik.

+ +
+ +

+ Jay's blog +

+
+ +
+
+ + + + + + + +

React Hook Form: A Leaky Abstraction

+ +

+ + + +

+ + +

I have used a handful of helper libraries for wrangling form state in React over the course of my career. In my current job, we're using React Hook Form1, specifically v6, although I think my story might still apply to those using the newer v7 branch. I enjoy working with it for the most part. I've found it to be a lighter-weight alternative to Formik.

However, this past week I ran into a weird edge case. To set the stage, a friend and colleague2 was working on a ticket to add a field to an existing form that's powered by React Hook Form. The new field was a <select> style dropdown menu with dynamically populated options based on the user's configuration. This field is required. However, most users will probably only have one option. In that case, we chose not to show this new field at all. Instead, that field will automatically default to the only option available to that user.

Here's a tiny example:

const { groups } = useUser();
@@ -375,22 +420,22 @@ 

React Hook Form: A Leaky Abstraction

-

-- React Hook Form docs for useForm, specifically under defaultValues

+

-- React Hook Form docs for useForm, specifically under defaultValues

This little tidbit is what tipped me off to this situation.3

There's also a clue in the FAQs for v7 in the answer for the question, "How to work with modal or tab forms?"

It's important to understand React Hook Form embraces native form behavior by storing input state inside each input (except custom register at useEffect). One of the common misconceptions is that when working with modal or tab forms, by mounting and unmounting form/inputs that inputs state will remain. That is incorrect. Instead, the correct solution would be to build a new form for your form inside each modal or tab and capture your submission data in local or global state and then do something with the combined data.

-

-- React Hook Form FAQs, "How to work with modal or tab forms?"

+

-- React Hook Form FAQs, "How to work with modal or tab forms?"

These bits of information, pieced together, answer the question of why every piece of data in a React Hook Form-based form must have an input associated with it, even if it's a hidden input.

-

A Leaky Abstraction

I don't use the term "leaky abstraction" lightly because it has become a generic insult thrown around by developers online, lodged against any tool or library they don't like. However, this is a textbook leaky abstraction in my mind.

-

The Abstraction

I'm using React Hook Form to reduce the boilerplate of form state management. I could write my own state management for any given form using only React built-ins like useReducer or I guess a dozen or so useState instances. But it's tedious, repetitive, and error-prone. If I choose to build this using React built-ins, the state of the form and the form inputs are completely separated from each other. If I should choose to conditionally render any of my input elements, the state representing the data of those inputs is unaffected. The data and its visual representation aren't dependent on one another.4

+

A Leaky Abstraction

I don't use the term "leaky abstraction" lightly because it has become a generic insult thrown around by developers online, lodged against any tool or library they don't like. However, this is a textbook leaky abstraction in my mind.

+

The Abstraction

I'm using React Hook Form to reduce the boilerplate of form state management. I could write my own state management for any given form using only React built-ins like useReducer or I guess a dozen or so useState instances. But it's tedious, repetitive, and error-prone. If I choose to build this using React built-ins, the state of the form and the form inputs are completely separated from each other. If I should choose to conditionally render any of my input elements, the state representing the data of those inputs is unaffected. The data and its visual representation aren't dependent on one another.4

I would assume when I'm using a form state management library, it's doing this same stuff for me behind the scenes. It's just handling all of that tedious boilerplate code for me, but I assume the semantics, like a separation between data and view, would remain the same.

-

The Leak

As we saw, my assumptions were incorrect. As the docs said, React Hook Form depends intimately upon the input elements on the page to hold its state. This is an implementation detail of the library that you generally don't need to know or care about... until you do.

+

The Leak

As we saw, my assumptions were incorrect. As the docs said, React Hook Form depends intimately upon the input elements on the page to hold its state. This is an implementation detail of the library that you generally don't need to know or care about... until you do.

I'm willing to believe that my form is the edge case here. Most forms on the web can be built using React Hook Form and you'll never need to know about these implementation details. Most forms probably show all inputs every render. But we were improving UX so that users don't need to care about a required dropdown input if there's only one option to select from.

I want to make it clear here that I'm not calling React Hook Form a leaky abstraction as an insult. I still mostly like the library, even if it's not built the same way I would choose to build it. I'm going to continue using it. I still prefer it to Formik. But this is an implementation detail, or perhaps a consequential design philosophy, that users should be aware of.

-
+
  1. Because I regularly confuse the name as react-form-hook, I've given up and lovingly refer to it as react-man-door-hand-hook-car-door instead. I'm sure my coworkers have become tired of this joke.

  2. @@ -399,22 +444,39 @@

    The Leak

    As we saw, my assumptions were incorrect. As t

  3. It's been a couple of years since I've used Formik, but this is roughly how it operates IIRC. When I switched to working on a codebase using React Hook Form, I assumed this was still the case.

-

-#react -

-
- - - - - + + + + + + +

+ + #react + +

+ + + + + + + + + +
+ -
- + + + + +
+ \ No newline at end of file diff --git a/jaysherby.com/resume/index.html b/jaysherby.com/resume/index.html index 29a90d4..30649af 100644 --- a/jaysherby.com/resume/index.html +++ b/jaysherby.com/resume/index.html @@ -1,12 +1,20 @@ - - - -Résumé | Jay's blog - - + + + + + + + Résumé | Jay's blog + + + + + + + + @@ -43,6 +52,8 @@ ma..."> + + @@ -62,6 +73,9 @@ ma..."> + + + - - - + + + -
- -

-Jay's blog -

-
- -
-
-
-

Jay Sherby

-

Staff Software Developer

-resume2024@jaysherby.com +

Jay Sherby

+

Staff Software Developer

+ resume2024@jaysherby.com

Employment

-
-Staff Software Engineer, Vyne Medical -Mar 2022 — Present -
-
-
    -
  • Stabilized Onederful product code base post-acquisition of start-up
  • -
  • Stepped into a leadership role post-team turnover, driving the advancement and maintenance of the Connected Care app
  • -
  • Led end-to-end development of patient form feature, enhancing application capabilities
  • -
-
-
-Senior Software Engineer, Fast Radius -Mar 2020 — Feb 2022 -
-
-
    -
  • Developed UPS API integration for realistic shipping estimates, enhancing customer experience and streamlining order processing
  • -
  • Optimized app performance for large orders, ensuring scalability and system stability
  • -
      -
-
-Lead Software Engineer, Jellyvision -Feb 2014 — Jan 2020 -
-
-
    -
  • Developed "The Calculator": service for optimized medical and dental plan choices, introducing robust testing tools for seamless updates
  • -
  • Created "Rogue": Game-changing tool accelerated product customizations, revolutionizing billing structures and streamlining workflows
  • -
  • Pioneered "Talent Show": Innovative low-code/no-code authoring tool for pre-scripted interactive conversations
  • -
      -
-
- -Software Developer, Re-Factor, Freelance - -Nov 2018 — Jul 2019 -
-
-
    -
  • Enhanced job recruiter application with modern tech (Typescript, React, GraphQL), optimizing feature development
  • -
-
-
-Software Developer, Threadless -May 2013 — Jan 2014 -
-
-
    -
  • Developed artist dashboard for real-time analytics and sales data, enhancing user experience and artist engagement
  • -
-
-
-Software Developer, Mintel -Aug 2011 — May 2013 -
-
-
-Associate Developer II, Applied Systems -Sep 2010 — Jul 2011 -
-
+
+ Staff Software Engineer, Vyne Medical + Mar 2022 — Present +
+
+
    +
  • Stabilized Onederful product code base post-acquisition of start-up
  • +
  • Stepped into a leadership role post-team turnover, driving the advancement and maintenance of the Connected Care app
  • +
  • Led end-to-end development of patient form feature, enhancing application capabilities
  • +
+
+
+ Senior Software Engineer, Fast Radius + Mar 2020 — Feb 2022 +
+
+
    +
  • Developed UPS API integration for realistic shipping estimates, enhancing customer experience and streamlining order processing
  • +
  • Optimized app performance for large orders, ensuring scalability and system stability
  • +
      +
+
+ Lead Software Engineer, Jellyvision + Feb 2014 — Jan 2020 +
+
+
    +
  • Developed "The Calculator": service for optimized medical and dental plan choices, introducing robust testing tools for seamless updates
  • +
  • Created "Rogue": Game-changing tool accelerated product customizations, revolutionizing billing structures and streamlining workflows
  • +
  • Pioneered "Talent Show": Innovative low-code/no-code authoring tool for pre-scripted interactive conversations
  • +
      +
+
+ + Software Developer, Re-Factor, Freelance + + Nov 2018 — Jul 2019 +
+
+
    +
  • Enhanced job recruiter application with modern tech (Typescript, React, GraphQL), optimizing feature development
  • +
+
+
+ Software Developer, Threadless + May 2013 — Jan 2014 +
+
+
    +
  • Developed artist dashboard for real-time analytics and sales data, enhancing user experience and artist engagement
  • +
+
+
+ Software Developer, Mintel + Aug 2011 — May 2013 +
+
+
+ Associate Developer II, Applied Systems + Sep 2010 — Jul 2011 +
+

Skills

    -
  • Docker
  • -
  • Elixir
  • -
  • JavaScript
  • -
  • MySQL
  • -
  • Node.js
  • -
  • PHP
  • -
  • PostgreSQL
  • -
  • Python
  • -
  • React
  • -
  • Typescript
  • +
  • Docker
  • +
  • Elixir
  • +
  • JavaScript
  • +
  • MySQL
  • +
  • Node.js
  • +
  • PHP
  • +
  • PostgreSQL
  • +
  • Python
  • +
  • React
  • +
  • Typescript

Education

-
-DeVry University, Merrillville, IN -2007 — 2010 -
-
B.Sc. in Computer Information Systems, May 2010
+
+ DeVry University, Merrillville, IN + 2007 — 2010 +
+
B.Sc. in Computer Information Systems, May 2010
-
- - - - - + + + + + + + + + + + + + + +
+ -
- + + + + + + \ No newline at end of file diff --git a/jaysherby.com/sequelize-and-the-disappearing-id-column/index.html b/jaysherby.com/sequelize-and-the-disappearing-id-column/index.html index f54f813..acd040a 100644 --- a/jaysherby.com/sequelize-and-the-disappearing-id-column/index.html +++ b/jaysherby.com/sequelize-and-the-disappearing-id-column/index.html @@ -1,17 +1,26 @@ - - - -Sequelize And The Disappearing ID Column | Jay's blog - - + + + + + + + Sequelize And The Disappearing ID Column | Jay's blog + + + + + + + + @@ -21,6 +30,8 @@ Here's some example model..."> + + @@ -29,6 +40,9 @@ Here's some example model..."> + + + - - - - + + + -
- -

-Jay's blog -

-
- -
-
-

Sequelize And The Disappearing ID Column

-

- - - -

-

I recently fixed a bug in some code that uses Sequelize. Let me set the scene and let's see if you can figure out what happened.

+ +
+ +

+ Jay's blog +

+
+ +
+
+ + + + + + + +

Sequelize And The Disappearing ID Column

+ +

+ + + +

+ + +

I recently fixed a bug in some code that uses Sequelize. Let me set the scene and let's see if you can figure out what happened.

Here's some example models that are sufficient to illustrate the situation I faced. I'll use a typical example domain. There is a table for books, a table for authors, and a join table to associate them since books can have multiple authors at once.

// Book.model.ts
 
@@ -401,7 +446,7 @@ 

Sequelize And The Disappearing ID Column

] }
-

Fantastic. No surprises there.

+

Fantastic. No surprises there.

Fast forward a couple of months and a bug was reported. An error was occurring because of a missing ID field in the results of that same query.

Let's run the query again and see what we get.

--- before.json 2023-12-23 20:50:56.638119488 +0000
@@ -444,9 +489,9 @@ 

Sequelize And The Disappearing ID Column

) );
-

But that didn't explain why the id was suddenly missing. It took a git bisect script for me to figure it out. Git's bisect command is a very powerful tool and I recommend you add it to your tool belt if you haven't already.

+

But that didn't explain why the id was suddenly missing. It took a git bisect script for me to figure it out. Git's bisect command is a very powerful tool and I recommend you add it to your tool belt if you haven't already.

If you want to try to figure it out for yourself, stop here.

-
+

After running git bisect, the culprit was a seemingly innocent change to the Author.model.ts file. A developer had added belongsToMany to the Author model to make it easier to traverse the relationship between Author and Book.

// Author.model.ts
 
@@ -468,22 +513,39 @@ 

Sequelize And The Disappearing ID Column

Using the belongsToMany function to mark the BookAuthor model definitively as a join table in the eyes of Sequelize causes Sequelize to hide the id column. Sequelize uses the foreign keys of join tables as a complex unique primary key.

When you look at it from the point of view of Sequelize's happy path, this makes complete sense. This is how Sequelize thinks join tables should be defined. When you look at it through a historical lens, it's surprising behavior that a change to some other model should cause a column to disappear from the results of a query.

-

-#sequelize -

-
- - - - - + + + + + + +

+ + #sequelize + +

+ + + + + + + + + +
+ -
- + + + + +
+ \ No newline at end of file diff --git a/jaysherby.com/terraform-cloud-cdktf-auth0/index.html b/jaysherby.com/terraform-cloud-cdktf-auth0/index.html index 56486d4..e884e6a 100644 --- a/jaysherby.com/terraform-cloud-cdktf-auth0/index.html +++ b/jaysherby.com/terraform-cloud-cdktf-auth0/index.html @@ -1,17 +1,26 @@ + - - -Terraform Cloud + CDKTF + Auth0 | Jay's blog - - + + + + + + Terraform Cloud + CDKTF + Auth0 | Jay's blog + + + + + + + @@ -21,6 +30,8 @@ But since you're still reading, I assume you find yoursel..."> + + @@ -29,6 +40,9 @@ But since you're still reading, I assume you find yoursel..."> + + + - - - - + + + -
- -

-Jay's blog -

-
- -
-
-

Terraform Cloud + CDKTF + Auth0

-

- - - -

-

If you want my advice on how to manage Auth0 using CDKTF and Terraform Cloud, here it is: don't.

-

But since you're still reading, I assume you find yourself in the regrettable situation of needing to support just such a setup. Here are some little nuggets I've learned the hard way that don't seem to be documented elsewhere.

-

Rules, Hooks, and String Interpolation

Rules and hooks are nifty features of Auth0 that allow you to run custom Javascript code on Auth0's servers in response to various events, like when a user signs up, logs in, etc. From what I can tell, they both serve roughly the same purpose, although it seems like hooks are newer, and Auth0 would probably like to deprecate the rules feature very much. They're both unpleasant to test, just like most kinds of code that run in other people's playgrounds.

-

You may find yourself wanting to deploy the code for your rules and hooks using Terraform. In the setup I inherited, the CDKTF scripts, written in Typescript, read source files for rules and hooks using fs.readFileSync, and eventually insert their contents as a string into the JSON config that CDKTF synthesizes.

+ +
+ +

+ Jay's blog +

+
+ +
+
+ + + + + + + +

Terraform Cloud + CDKTF + Auth0

+ +

+ + + +

+ + +

If you want my advice on how to manage Auth0 using CDKTF and Terraform Cloud, here it is: don't.

+

But since you're still reading, I assume you find yourself in the regrettable situation of needing to support just such a setup. Here are some little nuggets I've learned the hard way that don't seem to be documented elsewhere.

+

Rules, Hooks, and String Interpolation

Rules and hooks are nifty features of Auth0 that allow you to run custom Javascript code on Auth0's servers in response to various events, like when a user signs up, logs in, etc. From what I can tell, they both serve roughly the same purpose, although it seems like hooks are newer, and Auth0 would probably like to deprecate the rules feature very much. They're both unpleasant to test, just like most kinds of code that run in other people's playgrounds.

+

You may find yourself wanting to deploy the code for your rules and hooks using Terraform. In the setup I inherited, the CDKTF scripts, written in Typescript, read source files for rules and hooks using fs.readFileSync, and eventually insert their contents as a string into the JSON config that CDKTF synthesizes.

Terraform Cloud sometimes reports errors during the planning step that say things like the following, while pointing the finger at the value for a rule's or hook's "script" key.

A reference to a resource type must be followed by at least one attribute access, specifying the resource name.

@@ -340,53 +385,71 @@

Rules, Hooks, and String Interpola
const domain = `app.${environment}.my-really-cool-app.com`;
 

Terraform's parser sees ${ in your string, which happens to be its string interpolation syntax as well, and falls flat on its face trying to figure out what you're trying to accomplish.

-

There are ways around this. According to the docs, you can escape ${ with $${.1

-

But are you really going to put ${ in your Javascript? Absent of this context, ${ looks like a bug! You could leave a comment explaining the extra $. But after the code is deployed, the comment will remain and the extra $ would disappear. The comment wouldn't make sense if you looked at the code form inside the Auth0 management console.

-

IMHO, doing this is asking for trouble. Plus it feels gross.

-

Worse yet, what if the name of the variable inside your interpolated string (environment in the example above) does mean something to Terraform and it replaces it with a value? The code would probably throw an error if the replaced value wasn't a variable in your script. What if it was!? In any case, the code deployed won't match the code in your repository. Good luck chasing down that bug!

+

There are ways around this. According to the docs, you can escape ${ with $${.1

+

But are you really going to put ${ in your Javascript? Absent of this context, ${ looks like a bug! You could leave a comment explaining the extra $. But after the code is deployed, the comment will remain and the extra $ would disappear. The comment wouldn't make sense if you looked at the code form inside the Auth0 management console.

+

IMHO, doing this is asking for trouble. Plus it feels gross.

+

Worse yet, what if the name of the variable inside your interpolated string (environment in the example above) does mean something to Terraform and it replaces it with a value? The code would probably throw an error if the replaced value wasn't a variable in your script. What if it was!? In any case, the code deployed won't match the code in your repository. Good luck chasing down that bug!

The workaround I chose was to avoid template string interpolation all together and revert back to string concatenation like we did in the days before template literals were introduced.

const domain = 'app.' + environment + '.my-really-cool-app.com';
 
-

Terraform Variables vs Environment Variables

Using environment variables when writing your config with CDKTF and deploying with Terraform Cloud can be fraught, to say the least. There's at least one very good reason to use environment variables when using the Auth0 provider: they recommend it in the docs.

-

Let's clarify something first. Because of how CDKTF works, environment variables can be inserted in at least two stages: synthesis and planning.

-

If you do anything in your code using process.env to get at environment variables, this is at best confusing, at worst incorrect. This is how you'd read environment variables at synthesis time, which is probably not what you actually want. Doing this would affect what is output to your Terraform JSON when you run CDKTF's synthesis step. It essentially becomes a hard-coded value from the point of view of Terraform.

+

Terraform Variables vs Environment Variables

Using environment variables when writing your config with CDKTF and deploying with Terraform Cloud can be fraught, to say the least. There's at least one very good reason to use environment variables when using the Auth0 provider: they recommend it in the docs.

+

Let's clarify something first. Because of how CDKTF works, environment variables can be inserted in at least two stages: synthesis and planning.

+

If you do anything in your code using process.env to get at environment variables, this is at best confusing, at worst incorrect. This is how you'd read environment variables at synthesis time, which is probably not what you actually want. Doing this would affect what is output to your Terraform JSON when you run CDKTF's synthesis step. It essentially becomes a hard-coded value from the point of view of Terraform.

What you probably want to do, and what the Auth0 provider documentation is referring to, is environment variables at planning time.

The environment variables the docs refer to, AUTH0_DOMAIN, AUTH0_CLIENT_ID, and AUTH0_CLIENT_SECRET, are not directly accessible to your Terraform config.2 I need to set these values as secrets for one of my hooks.

-

The Most Correct™ Way

As I said, you can't directly access those environment variables for reasons I'll soon explain. Terraform-literate readers will already know that the Most Correct™ way to access the values would be indirectly via the resources that use them. This is The Terraform Way.

-

If this configuration was written in HCL, this would be comically easy. But we're using CDKTF for reasons I can't begin to explain.

-

You should be able to access AUTH0_DOMAIN, AUTH0_CLIENT_ID, and AUTH0_CLIENT_SECRET via Auth0Provider#domainInput, Client#clientId, and Client#clientSecret, respectively. But my config isn't written in a way that would make this easy. It would require a ton of refactoring that I don't have the time or patience for at the moment.

-

The Quick 'N' Hacky Way

Since I can't access those particular environment variables directly, and I'm unable to access the values indirectly via the resources that consume them, I chose to add some duplicate variables that I could access like any other variable.

+

The Most Correct™ Way

As I said, you can't directly access those environment variables for reasons I'll soon explain. Terraform-literate readers will already know that the Most Correct™ way to access the values would be indirectly via the resources that use them. This is The Terraform Way.

+

If this configuration was written in HCL, this would be comically easy. But we're using CDKTF for reasons I can't begin to explain.

+

You should be able to access AUTH0_DOMAIN, AUTH0_CLIENT_ID, and AUTH0_CLIENT_SECRET via Auth0Provider#domainInput, Client#clientId, and Client#clientSecret, respectively. But my config isn't written in a way that would make this easy. It would require a ton of refactoring that I don't have the time or patience for at the moment.

+

The Quick 'N' Hacky Way

Since I can't access those particular environment variables directly, and I'm unable to access the values indirectly via the resources that consume them, I chose to add some duplicate variables that I could access like any other variable.

The official way to get values into Terraform at planning time via environment variables is to use them in your CDKTF configuration just like any other variable (using the TerraformVariable class), and set an environment variable in Terraform Cloud with the same name but with a TF_VAR_ prefix.

-

That meant that for my use case, I needed both AUTH0_DOMAIN and TF_VAR_AUTH0_DOMAIN set in my Terraform Cloud variables config page,3 along with the remaining two variables mentioned earlier, using this same pattern. If I only set AUTH0_DOMAIN, the Auth0 provider will work, but the AUTH0_DOMAIN Terraform variable defaulted to an empty string! If I set TF_VAR_AUTH0_DOMAIN but not AUTH0_DOMAIN, I got the following error during the planning phase.

+

That meant that for my use case, I needed both AUTH0_DOMAIN and TF_VAR_AUTH0_DOMAIN set in my Terraform Cloud variables config page,3 along with the remaining two variables mentioned earlier, using this same pattern. If I only set AUTH0_DOMAIN, the Auth0 provider will work, but the AUTH0_DOMAIN Terraform variable defaulted to an empty string! If I set TF_VAR_AUTH0_DOMAIN but not AUTH0_DOMAIN, I got the following error during the planning phase.

The argument "domain" is required, but no definition was found.

-

I chose to set both AUTH0_DOMAIN and TF_VAR_AUTH0_DOMAIN as environment variables in Terraform Cloud because that's ever-so-slightly less confusing IMHO than two variables, both named AUTH0_DOMAIN, one a Terraform variable and the other an environment variable. I figure someone would be more likely to erroneously delete one of the two copies in the future if both variables shared the same exact name but were different variable types. As always, notes and documentation are your best friends.

-
+

I chose to set both AUTH0_DOMAIN and TF_VAR_AUTH0_DOMAIN as environment variables in Terraform Cloud because that's ever-so-slightly less confusing IMHO than two variables, both named AUTH0_DOMAIN, one a Terraform variable and the other an environment variable. I figure someone would be more likely to erroneously delete one of the two copies in the future if both variables shared the same exact name but were different variable types. As always, notes and documentation are your best friends.

+
  1. I've seen some bizarre alternative ways of escaping ${ suggested on Stack Overflow, but all of my criticism still apply.

  2. -
  3. There are ways you can expose all environment variables as Terraform variables without having to know the variables' keys beforehand, but it's not what The Architects intended. I've only seen examples of these techniques in HCL. It might be possible to accomplish in CDKTF, but you'd be fighting your chosen tools so hard that I'd question the whole endeavor. Just because you can doesn't mean you should.

  4. -
  5. In case you were wondering, you still need to add the TF_VAR_ prefix yourself in Terraform Cloud. That wasn't immediately clear to me. I'm guessing it's to support environment variables that don't start with TF_VAR_ since providers (like Auth0 with AUTH0_DOMAIN, etc.) aren't required to conform to that pattern. Don't forget TF_VAR_

  6. +
  7. There are ways you can expose all environment variables as Terraform variables without having to know the variables' keys beforehand, but it's not what The Architects intended. I've only seen examples of these techniques in HCL. It might be possible to accomplish in CDKTF, but you'd be fighting your chosen tools so hard that I'd question the whole endeavor. Just because you can doesn't mean you should.

  8. +
  9. In case you were wondering, you still need to add the TF_VAR_ prefix yourself in Terraform Cloud. That wasn't immediately clear to me. I'm guessing it's to support environment variables that don't start with TF_VAR_ since providers (like Auth0 with AUTH0_DOMAIN, etc.) aren't required to conform to that pattern. Don't forget TF_VAR_

-

-#auth0 -#terrorform -

-
- - - - - + + + + + + +

+ + #auth0 + + #terrorform + +

+ + + + + + + + + +
+ -
- + + + + +
+ \ No newline at end of file diff --git a/jaysherby.com/the-two-most-popular-missing-typescript-features/index.html b/jaysherby.com/the-two-most-popular-missing-typescript-features/index.html index 4a2ae98..ed32792 100644 --- a/jaysherby.com/the-two-most-popular-missing-typescript-features/index.html +++ b/jaysherby.com/the-two-most-popular-missing-typescript-features/index.html @@ -1,17 +1,26 @@ - - - -The Two Most Popular Missing Typescript Features | Jay's blog - - + + + + + + + The Two Most Popular Missing Typescript Features | Jay's blog + + + + + + + + @@ -21,6 +30,8 @@ There are two features I think should be in Typescript that are currently missing. It's not just opinion, either. When I was researching this blog post,..."> + + @@ -29,6 +40,9 @@ There are two features I think should be in Typescript that are currently missing. It's not just opinion, either. When I was researching this blog post,..."> + + + - - - - + + + -
- -

-Jay's blog -

-
- -
-
-

The Two Most Popular Missing Typescript Features

-

- - - -

-

Original art, the suspense is terrible, I hope it'll last

+ +
+ +

+ Jay's blog +

+
+ +
+
+ + + + + + + +

The Two Most Popular Missing Typescript Features

+ +

+ + + +

+ + +

Original art, the suspense is terrible, I hope it'll last

There are two features I think should be in Typescript that are currently missing. It's not just my opinion, either. When I was researching this blog post, I discovered that the tickets requesting these two features are the most discussed tickets with the "suggestion" tag still open. If I could wave a magic wand to change Typescript in any way, I'd add these two features.

-

#13219 throws clause and typed catch clause

This missing feature came up in discussion on a pull request of mine at work this week. A particular function had a return type of Promise<boolean> and included a try...catch statement in its body. A friend and colleague1 left a comment asking why the possible error types weren't represented in the return type of the function. Maybe something like Promise<boolean, AppException>?

+

#13219 throws clause and typed catch clause

This missing feature came up in discussion on a pull request of mine at work this week. A particular function had a return type of Promise<boolean> and included a try...catch statement in its body. A friend and colleague1 left a comment asking why the possible error types weren't represented in the return type of the function. Maybe something like Promise<boolean, AppException>?

The answer is that the Promise type doesn't support a second type variable to describe the value returned on rejection of a promise.

As I suspected, the lack of a rejection type mirrors how caught error types are handled. (Or should I say, not handled?)

try {
@@ -341,7 +386,7 @@ 

2

-

I'd prefer it if Typescript could tell me all the possible types that might be thrown by a particular function or block of code. And I'm not alone. 1,349 people have given the 👍 reaction to microsoft/Typescript#13219.

+

I'd prefer it if Typescript could tell me all the possible types that might be thrown by a particular function or block of code. And I'm not alone. 1,349 people have given the 👍 reaction to microsoft/Typescript#13219.

The syntax suggested mirrors a feature already present in crusty, old Java.

 public static void fetchFile() throws IOException, NullPointerException {
     // code that can throw IOException
@@ -358,9 +403,9 @@ 

// handle NullPointerException appropriately }

-

I vastly prefer this syntax over a single catch block containing repeated if...else blocks with e instanceof AppError conditions to detect each type. It's important to note that microsoft/Typescript#13219 does not include this new try...catch syntax. I couldn't find a ticket requesting such a feature. But adding the throws annotation to function signatures might set the stage for it.

+

I vastly prefer this syntax over a single catch block containing repeated if...else blocks with e instanceof AppError conditions to detect each type. It's important to note that microsoft/Typescript#13219 does not include this new try...catch syntax. I couldn't find a ticket requesting such a feature. But adding the throws annotation to function signatures might set the stage for it.

When might we see the throws clause in Typescript? I wouldn't hold my breath. This ticket has been open for 7 years! But a boy can dream, can't he?

-

#202 Nominal types

Typescript has a structural type system. What does this mean?

+

#202 Nominal types

Typescript has a structural type system. What does this mean?

interface Car {
   name: string;
   age: number;
@@ -440,7 +485,7 @@ 

}

This C program is almost identical to the Typescript code, except that I've made the Car and Dog types structurally identical to one another. C does not let me pass a Dog to the announce_car function.3 If I really wanted to pass a Dog to announce_car in a nominally typed language, I'd have to cast it to a Car or manually create a new Car with the values from my Dog.

-

IMHO, unless you already understand the difference between structural typing and nominal typing, microsoft/Typescript#202 doesn't do a very good job explaining it. But the feature being requested is the ability to opt into nominal typing for a given type. If I could specify that Car is a nominal type and a function expects a Car, you'd need to either give it a Car or cast the value you're passing into a Car first.

+

IMHO, unless you already understand the difference between structural typing and nominal typing, microsoft/Typescript#202 doesn't do a very good job explaining it. But the feature being requested is the ability to opt into nominal typing for a given type. If I could specify that Car is a nominal type and a function expects a Car, you'd need to either give it a Car or cast the value you're passing into a Car first.

But... why? Why do people want this feature so badly?

Let me give you the most concrete example I've seen for why nominal typing could be so helpful.

type CarID = number;
@@ -486,36 +531,54 @@ 

If I was really, really sure that I was doing the right thing, I could make Typescript accept it by casting, like this:

updateCarName(bruno.id as CarID, "Chevy Bel Air");
 
-

This may seem like an odd concept, having explicit nominal types in an otherwise structural type system. I tried searching for prior art, but I didn't really find anything that works quite like what's being proposed in microsoft/Typescript#202. In Elm, you can achieve a similar effect using algebraic data types. Typescript doesn't support algebraic data types and it likely never will as that's a whole different beast.4 Flow treats classes as nominal types, but that's all. It's not something you can opt into for basic types like numbers.

-

If you read through the microsoft/Typescript#202 ticket, you'll see plenty of people's attempts to achieve the desired effect using Typescript as it exists today. There are also plenty of options available on NPM. The general technique goes by many names: opaque types, branded types, tagged types, flavored types, etc. There are multiple variations on the theme that have their own advantages and drawbacks. While the more mainstream implementations are serviceable, I'd wager the concept would be easier to work with as a first-class feature of the language.5

+

This may seem like an odd concept, having explicit nominal types in an otherwise structural type system. I tried searching for prior art, but I didn't really find anything that works quite like what's being proposed in microsoft/Typescript#202. In Elm, you can achieve a similar effect using algebraic data types. Typescript doesn't support algebraic data types and it likely never will as that's a whole different beast.4 Flow treats classes as nominal types, but that's all. It's not something you can opt into for basic types like numbers.

+

If you read through the microsoft/Typescript#202 ticket, you'll see plenty of people's attempts to achieve the desired effect using Typescript as it exists today. There are also plenty of options available on NPM. The general technique goes by many names: opaque types, branded types, tagged types, flavored types, etc. There are multiple variations on the theme that have their own advantages and drawbacks. While the more mainstream implementations are serviceable, I'd wager the concept would be easier to work with as a first-class feature of the language.5

How does this feature request compare to the throws feature request from the first half of this blog post? It has fewer 👍 reactions, but more discussion. As the issue number implies, it's been around longer. A whole 9 years! Looking at the request honestly, I think nominal types would be less impactful for the average developer than the throws feature. But it would still be a nice addition because it could help eliminate an entire class of bugs if used diligently.

-
+
  1. Shout out to Emily! Thanks for the great question and the inspiration for this blog post. ❤️

  2. Interestingly, you can't throw void. Thank goodness for small favors, I guess.

  3. Well, it would let me pass a Dog to announce_car if I hadn't compiled it with the -Werror flag. In that case, the compiler emits the same message as a warning and lets me do it anyway, which does work. But it only works because the types are literally the same. If I add a breed field to Dog after name but before age and set breed to "terrier", announce_car will emit "The Bruno is a 4195924 year old car." But that's beyond the scope of the point I'm trying to make here.

  4. FWIW, I love algebraic data types. I think they're more expressive than what Typescript currently has. But I don't think ADTs can map cleanly onto JavaScript's type system, meaning it's incompatible with Typescript's design goals.

  5. -
  6. The way most opaque type implementations work amounts to lying to the type system. Take the implementation from ts-essentials for example. The implementation is only 9 lines long. It's got some interesting conditional types in there. I'm not exactly sure what bug it's trying to protect against. At the end of the day, the actual, literal type of Opaque<number, 'CarID'> would be number & { [__OPAQUE_TYPE__]: 'CarID' }. But that's a lie. Numeric literals are "turned into" the opaque type via assertion: 5 as CarID. There is no __OPAQUE_TYPE__ on the value. (1 as UserID)[__OPAQUE_TYPE__] is valid according to the compiler. The ts-essentials implementation prevents us from being able to call its bluff by declaring __OPAQUE_TYPE__ as a unique symbol that's never actually created or given a value, much less exported, so it's impossible to actually access that value since there's no way to access it. Try it in the playground to see what I mean. I've copied and pasted the code from ts-essentials. It compiles fine but throws an error at runtime.

  7. +
  8. The way most opaque type implementations work amounts to lying to the type system. Take the implementation from ts-essentials for example. The implementation is only 9 lines long. It's got some interesting conditional types in there. I'm not exactly sure what bug it's trying to protect against. At the end of the day, the actual, literal type of Opaque<number, 'CarID'> would be number & { [__OPAQUE_TYPE__]: 'CarID' }. But that's a lie. Numeric literals are "turned into" the opaque type via assertion: 5 as CarID. There is no __OPAQUE_TYPE__ on the value. (1 as UserID)[__OPAQUE_TYPE__] is valid according to the compiler. The ts-essentials implementation prevents us from being able to call its bluff by declaring __OPAQUE_TYPE__ as a unique symbol that's never actually created or given a value, much less exported, so it's impossible to actually access that value since there's no way to access it. Try it in the playground to see what I mean. I've copied and pasted the code from ts-essentials. It compiles fine but throws an error at runtime.

-

-#programming language theory -#typescript -

-
- - - - - + + + + + + +

+ + #programming language theory + + #typescript + +

+ + + + + + + + + +
+ -

- + + + + +
+ \ No newline at end of file diff --git a/jaysherby.com/tips-for-becoming-a-pod-person/index.html b/jaysherby.com/tips-for-becoming-a-pod-person/index.html index e297a5a..87bc7e1 100644 --- a/jaysherby.com/tips-for-becoming-a-pod-person/index.html +++ b/jaysherby.com/tips-for-becoming-a-pod-person/index.html @@ -1,17 +1,26 @@ + - - -Tips For Becoming A Pod Person | Jay's blog - - + + + + + + Tips For Becoming A Pod Person | Jay's blog + + + + + + + @@ -21,6 +30,8 @@ I made the leap from Docker to Podman. Well... only on my personal laptop. Podman isn't a completely hassle-free, drop-in replacement for Docker. It's ..."> + + @@ -29,6 +40,9 @@ I made the leap from Docker to Podman. Well... only on my personal laptop. Podman isn't a completely hassle-free, drop-in replacement for Docker. It's ..."> + + + - - - - + + + -
- -

-Jay's blog -

-
- -
-
-

Tips For Becoming A Pod Person

-

- - - -

-

Original art, with apologies to Donald Sutherland

-

I made the leap from Docker to Podman. Well... only on my personal laptop. Podman isn't a completely hassle-free, drop-in replacement for Docker. It's damn close! Close enough that I'm willing to use it at home, but it's still finicky and different enough that I'd spend too much time futzing at work trying to use it while keeping everything Docker-compatible for my colleagues.

+ +
+ +

+ Jay's blog +

+
+ +
+
+ + + + + + + +

Tips For Becoming A Pod Person

+ +

+ + + +

+ + +

Original art, with apologies to Donald Sutherland

+

I made the leap from Docker to Podman. Well... only on my personal laptop. Podman isn't a completely hassle-free, drop-in replacement for Docker. It's damn close! Close enough that I'm willing to use it at home, but it's still finicky and different enough that I'd spend too much time futzing at work trying to use it while keeping everything Docker-compatible for my colleagues.

Here are some tips if, like me, you're coming from Docker and you just want to get productive.

-

I need Docker Compose

A large part of Docker's value to me comes from Docker Compose. If switching to Podman meant losing Docker Compose, I wouldn't have switched.

+

I need Docker Compose

A large part of Docker's value to me comes from Docker Compose. If switching to Podman meant losing Docker Compose, I wouldn't have switched.

Thankfully, Pop!_OS (and probably any other platforms that include Podman in their repos) has a package called podman-docker that satisfies packages that depend on Docker. Just make sure you install podman-docker before or at the same time as docker-compose so APT doesn't try to install Docker to satisfy Docker Compose's dependencies.

-

If you're feeling adventurous, Podman Compose is a thing. But it's not available via my OS's default repositories and Docker Compose is.

-

PermissionError: [Errno 13] Permission denied

I recently received this error when I tried running Docker Compose. I had already installed podman-docker like I mentioned. The solution was to add this to my .bash_profile:

+

If you're feeling adventurous, Podman Compose is a thing. But it's not available via my OS's default repositories and Docker Compose is.

+

PermissionError: [Errno 13] Permission denied

I recently received this error when I tried running Docker Compose. I had already installed podman-docker like I mentioned. The solution was to add this to my .bash_profile:

export DOCKER_HOST=unix:///run/user/$UID/podman/podman.sock
 
-

I need Docker Hub

I also wouldn't use Podman if it meant I lost Docker Hub.

+

I need Docker Hub

I also wouldn't use Podman if it meant I lost Docker Hub.

The easiest way to get access to Docker Hub with Podman is to write the following file to $HOME/.config/containers/registries.conf:1

[registries.search]
 registries = ['docker.io']
 
-

Potentially insufficient UIDs or GIDs available in user namespace

I had just installed Podman and I wanted use the NodeJS image from Docker Hub.

+

Potentially insufficient UIDs or GIDs available in user namespace

I had just installed Podman and I wanted use the NodeJS image from Docker Hub.

$ podman run -it --rm node
 Resolving "node" using unqualified-search registries (/home/jsherby/.config/containers/registries.conf)
 Trying to pull docker.io/library/node:latest...
@@ -371,11 +416,11 @@ 

Poten

ignore_chown_errors can be set to allow a non privileged user running with a single UID within a user namespace to run containers. The user can pull and use any image even those with multiple uids. Note multiple UIDs will be squashed down to the default uid in the container. These images will have no separation between the users in the container.

Although this setup will make Podman stop complaining, there's a good chance this will bite you in the ass later on, especially if you're trying to stay compatible with Docker.

-

Instead, I added my user to /etc/subuid and /etc/subgid. Here's what both files look like on my machine:3

+

Instead, I added my user to /etc/subuid and /etc/subgid. Here's what both files look like on my machine:3

jsherby:100000:65536
 

Then I ran podman system migrate and I was good to go.

-
+
  1. /etc/containers/registries.conf is the equivalent system-wide config file.

  2. @@ -383,23 +428,41 @@

    Poten
  3. When supporting namespaces for multiple users, the middle value needs to be offset so the namespaces don't overlap. Check the man pages that come with your local shadow package for details.

-

-#docker -#podman -

-
- - - - - + + + + + + +

+ + #docker + + #podman + +

+ + + + + + + + + +
+ -

- + + + + +
+ \ No newline at end of file diff --git a/jaysherby.com/typescript-show-me-the-whole-error/index.html b/jaysherby.com/typescript-show-me-the-whole-error/index.html index 8c7176a..d04c7f5 100644 --- a/jaysherby.com/typescript-show-me-the-whole-error/index.html +++ b/jaysherby.com/typescript-show-me-the-whole-error/index.html @@ -1,15 +1,24 @@ + - - -Typescript, Show Me The Whole Error! | Jay's blog - - + + + + + + Typescript, Show Me The Whole Error! | Jay's blog + + + + + + + @@ -17,12 +26,17 @@ + + + + + - - - + + + -
- -

-Jay's blog -

-
- -
-
-

Typescript, Show Me The Whole Error!

-

- - - -

-

Has this ever happened to you? Typescript throws an error about two types being incompatible, but the differing parts of the types is truncated from the error!

+ +
+ +

+ Jay's blog +

+
+ +
+
+ + + + + + + +

Typescript, Show Me The Whole Error!

+ +

+ + + +

+ + +

Has this ever happened to you? Typescript throws an error about two types being incompatible, but the differing parts of the types is truncated from the error!

This is the default behavior for some reason. I'm not a fan. I know long error messages scare a lot of developers, but I'd rather have a long and helpful error message than a short and useless one.

-

Good news! Error message salvation is one tsconfig.json setting away. Set noErrorTruncation to true and you'll be all set.

+

Good news! Error message salvation is one tsconfig.json setting away. Set noErrorTruncation to true and you'll be all set.

Personally, I'd rather opt in to truncation than opt out, but at least it's an easy fix.

-

-#typescript -

-
- - - - - + + + + + + +

+ + #typescript + +

+ + + + + + + + + +
+ -
- + + + + +
+ \ No newline at end of file diff --git a/jaysherby.com/use-promiseall-with-caution/index.html b/jaysherby.com/use-promiseall-with-caution/index.html index 5be3d4b..405664f 100644 --- a/jaysherby.com/use-promiseall-with-caution/index.html +++ b/jaysherby.com/use-promiseall-with-caution/index.html @@ -1,17 +1,26 @@ - - - -Use Promise.all With Caution | Jay's blog - - + + + + + + + Use Promise.all With Caution | Jay's blog + + + + + + + + @@ -21,6 +30,8 @@ First, let's briefly review how promises behave ..."> + + @@ -29,6 +40,9 @@ First, let's briefly review how promises behave ..."> + + + - - - - + + + -
- -

-Jay's blog -

-
- -
-
-

Use Promise.all With Caution

-

- - - -

-

A few weeks ago, I found two bugs in two different projects whose root causes were both careless use of Promise.all.

+ +
+ +

+ Jay's blog +

+
+ +
+
+ + + + + + + +

Use Promise.all With Caution

+ +

+ + + +

+ + +

A few weeks ago, I found two bugs in two different projects whose root causes were both careless use of Promise.all.

First, let's briefly review how promises behave when Promise.all is not used. For our scenario, let's imagine we need to make multiple network calls.

fetch('http://example.com/1')
   .then(() => fetch('http://example.com/2'))
@@ -360,11 +405,11 @@ 

Use Promise.all With Caution

items.map(item => item.update({ checked: true }) );
-

At first glance, this seems like a reasonable thing to do. Updating all the items simultaneously will speed this up, right? In actuality, this resulted in an error.

+

At first glance, this seems like a reasonable thing to do. Updating all the items simultaneously will speed this up, right? In actuality, this resulted in an error.

The first warning sign is that there is no limit on the number of items that belong to this particular user. Most of the time this will probably be fine. But what if a user has an idiosyncratic number of unchecked items? What if it's hundreds or thousands? Your server is going to attempt to run an unhealthy number of promises at once.2

-

In the real code, this was using the ORM Sequelize. Sequelize keeps a pool of database connections. Whenever you make a call to the database, a connection from the pool is used. Most of the time, only one connection is used per request. If you're doing something fancy with Promise.all, maybe more than one connection is used simultaneously per request. In this case, if the number of items exceeds the maximum number of connections allowed in the pool, you'll receive an error, which is exactly what happened.

+

In the real code, this was using the ORM Sequelize. Sequelize keeps a pool of database connections. Whenever you make a call to the database, a connection from the pool is used. Most of the time, only one connection is used per request. If you're doing something fancy with Promise.all, maybe more than one connection is used simultaneously per request. In this case, if the number of items exceeds the maximum number of connections allowed in the pool, you'll receive an error, which is exactly what happened.

To fix this, you could increase the maximum number of connections allowed in the pool. But how many is enough? Because items are created by users, there is no realistic maximum number. I chose to make the promises run serially instead.

-

I had run into similar issues regarding simultaneous database calls in the past and I had already added the Bluebird library to this particular project. It includes a function called mapSeries which does exactly what I need.

+

I had run into similar issues regarding simultaneous database calls in the past and I had already added the Bluebird library to this particular project. It includes a function called mapSeries which does exactly what I need.

import * as P from 'bluebird';
 
 // ...snip...
@@ -372,7 +417,7 @@ 

Use Promise.all With Caution

P.mapSeries(items, item => item.update({ checked: true });

If you don't like Bluebird, there are other libraries available to control the level of concurrency when Promise.all is used. Some of them even include the ability to specify concurrency limits higher than 1. I encourage the reader to experiment and see if they can roll their own function that takes an array of promises and awaits them serially.

-

The other bug I encountered was in a different project, but was once again a misapplication of Promise.all. In this case, it was in a database migration for use with the Knex ORM.

+

The other bug I encountered was in a different project, but was once again a misapplication of Promise.all. In this case, it was in a database migration for use with the Knex ORM.

export function up(knex) {
   return Promise.all([
     knex.schema.table('table_a', (table) => {
@@ -403,30 +448,48 @@ 

Use Promise.all With Caution

  • Don't use Promise.all if your promises all depend on a shared, limited resource.
  • Don't use Promise.all if the order in which the promises resolve matters.
  • -
    +
    1. I say "may" because it depends on what the promises are doing.

    2. My understanding is that most JavaScript runtimes have an upper limit to how many promises they'll execute in parallel when Promise.all is used, but I don't think any particular number is specified in the ECMAScript specification.

    -

    -#javascript -#typescript -

    -
    - - - - - + + + + + + +

    + + #javascript + + #typescript + +

    + + + + + + + + + +
    + -
    - + + + + +
    + \ No newline at end of file diff --git a/jaysherby.com/vintage-computer-festival-midwest-18/index.html b/jaysherby.com/vintage-computer-festival-midwest-18/index.html index cab0f58..729e749 100644 --- a/jaysherby.com/vintage-computer-festival-midwest-18/index.html +++ b/jaysherby.com/vintage-computer-festival-midwest-18/index.html @@ -1,18 +1,27 @@ - - - -Vintage Computer Festival Midwest 18 | Jay's blog - - + + + + + + + Vintage Computer Festival Midwest 18 | Jay's blog + + + + + + + + @@ -23,6 +32,8 @@ When I visited last year, the theme was Commodore 64 as it was the 40th anniversary of the popular mi..."> + + @@ -32,6 +43,9 @@ When I visited last year, the theme was Commodore 64 as it was the 40th anniversary of the popular mi..."> + + + - - - + + + -
    - -

    -Jay's blog -

    -
    - -
    -
    -

    Vintage Computer Festival Midwest 18

    -

    - - - -

    -

    I visited Vintage Computer Festival Midwest today. This is my second year attending.

    -

    Last year

    When I visited last year, the theme was Commodore 64 as it was the 40th anniversary of the popular micro's release. It was thrilling and a little overwhelming. I spent a fair amount of money on a C64 with all the modern bells and whistles. It needs a little love to strengthen the video signal, but it's in great condition otherwise.

    + +
    + +

    + Jay's blog +

    +
    + +
    +
    + + + + + + + +

    Vintage Computer Festival Midwest 18

    + +

    + + + +

    + + +

    I visited Vintage Computer Festival Midwest today. This is my second year attending.

    +

    Last year

    When I visited last year, the theme was Commodore 64 as it was the 40th anniversary of the popular micro's release. It was thrilling and a little overwhelming. I spent a fair amount of money on a C64 with all the modern bells and whistles. It needs a little love to strengthen the video signal, but it's in great condition otherwise.

    I'm embarrassed to say I haven't found the time to do the necessary repair, so the machine has been sitting atop my printer since I brought it home.

    -

    This year

    I was a bit more disciplined with my spending this year and managed to only walk out with a commemorative t-shirt. That's not to say I didn't have a ton of fun though. Let me share some of my favorite experiences from this year's festival.

    -

    Flipper Slipper on a Spectravideo SV-328

    I'd never heard of Spectravideo, nor Flipper Slipper before today. It was found, appropriately, at the "Obscure Computers" exhibit. Flipper Slipper is a Breakout-like game with an aquatic motif. Although the theme and elements were bizarre, the game controlled well and the twist of adding half circle paddles that can cause interesting ball physics was satisfying.

    +

    This year

    I was a bit more disciplined with my spending this year and managed to only walk out with a commemorative t-shirt. That's not to say I didn't have a ton of fun though. Let me share some of my favorite experiences from this year's festival.

    +

    Flipper Slipper on a Spectravideo SV-328

    I'd never heard of Spectravideo, nor Flipper Slipper before today. It was found, appropriately, at the "Obscure Computers" exhibit. Flipper Slipper is a Breakout-like game with an aquatic motif. Although the theme and elements were bizarre, the game controlled well and the twist of adding half circle paddles that can cause interesting ball physics was satisfying.

    Flipper Slipper was also released for the ColecoVision. Although I can't attest that the ColecoVision version has the same fluent controls, it's probably an easier platform to emulate if you want to give the game a try.

    -

    The Emergency Alert System

    The "Behind The Screens" table, returning from last year, caters to a niche but interesting subject. Featured were the hardware systems that generate automated cable channels like The Weather Channel and the Prevue Guide Channel, known as the TV Guide Channel on the cable package my parents had while I was growing up.

    +

    The Emergency Alert System

    The "Behind The Screens" table, returning from last year, caters to a niche but interesting subject. Featured were the hardware systems that generate automated cable channels like The Weather Channel and the Prevue Guide Channel, known as the TV Guide Channel on the cable package my parents had while I was growing up.

    The part of the exhibit that I had the most fun with was the Emergency Alert System hardware. A camera was pointed at the attendees. It fed into a vintage character generator you could use to overlay text. That was then output on a CRT TV. It's a clever gimmick for people who want to write a custom message on a selfie of themselves on TV.

    But within the loop was the EAS hardware. Next to the EAS box was a red telephone and an instruction card. By dialing a particular number, you were would choose a number corresponding to the type of emergency message you'd like to send, and record a voice message to be played during it. I chose "fire warning" and mumbled something about this being a test, just in case. Sure enough, those loud, familiar tone patterns played and an appropriate warning message appeared on the TV. I couldn't hear my message, but I'm sure it played and I just hadn't spoken loudly enough when I recorded it.

    It was a thrilling experience to trigger an emergency alert, even if it was just on a single television. In retrospect, I wish I had chosen a different type of message. A storm or tornado warning would be the most relatable. I did spot a choice in the list for a nuclear power plant warning. That sounds dramatic. I didn't get a chance to read the list of types completely, so maybe I missed some interesting options.

    -

    OS/2

    Although I didn't linger at the OS/2 exhibit, I was glad to see it return from last year. The first computer my family owned ran OS/2, so it's a real blast of nostalgia for me. I've independently collected a few different OS/2 installation media and other memorabilia including a beautiful enamel pin. Any acknowledgement of OS/2 is enough to put a smile on my face.

    -

    A step switch demonstration

    You may be surprised to find there's a significant number of telephony wonks at VCF Midwest. Apparently it's tradition to wire up a PBX private phone network that anyone can connect to if they bring a compatible device. Some machines are connected to host BBSes. Some connect automated phone systems, like the phone that was connected to the EAS I talked about earlier. Some people set up little Easter egg systems to encourage attendees to use wardialers to find them. It's a fascinating vintage computing subculture.

    -

    The exhibit that hosts the PBX hardware to power the phone system is "Shadytel MidWest Telephone CO", also returning from last year. The PBX system they employ is a vintage system from the 1970s, but it's already completely digital. However, the exhibit includes a two-phone demonstration of an older electro-mechanical step switch telephone exchange system. Here's a video with a very similar demonstration. It's impressively large with satisfying little movements and clicks. It feels very logical and elegant, given the technology available at the time of its creation. Chef's kiss, no notes.

    -

    Stuff for sale

      -
    • I got to see the Core64 in person. It's very cool. The ferrite beads are even smaller than you think they are.

      +

      OS/2

      Although I didn't linger at the OS/2 exhibit, I was glad to see it return from last year. The first computer my family owned ran OS/2, so it's a real blast of nostalgia for me. I've independently collected a few different OS/2 installation media and other memorabilia including a beautiful enamel pin. Any acknowledgement of OS/2 is enough to put a smile on my face.

      +

      A step switch demonstration

      You may be surprised to find there's a significant number of telephony wonks at VCF Midwest. Apparently it's tradition to wire up a PBX private phone network that anyone can connect to if they bring a compatible device. Some machines are connected to host BBSes. Some connect automated phone systems, like the phone that was connected to the EAS I talked about earlier. Some people set up little Easter egg systems to encourage attendees to use wardialers to find them. It's a fascinating vintage computing subculture.

      +

      The exhibit that hosts the PBX hardware to power the phone system is "Shadytel MidWest Telephone CO", also returning from last year. The PBX system they employ is a vintage system from the 1970s, but it's already completely digital. However, the exhibit includes a two-phone demonstration of an older electro-mechanical step switch telephone exchange system. Here's a video with a very similar demonstration. It's impressively large with satisfying little movements and clicks. It feels very logical and elegant, given the technology available at the time of its creation. Chef's kiss, no notes.

      +

      Stuff for sale

        +
      • I got to see the Core64 in person. It's very cool. The ferrite beads are even smaller than you think they are.

      • Altair 8800s! They're not cheap, so this is for the Serious Collector™. They were plentiful this year. But if you want in one the fun without the hefty price tag, there were also much cheaper clones built from new designs.

      • More Commodore 64 SX units in one place than I've ever seen! And that includes last year's C64 celebration. They were surprisingly affordable, and in very good condition. I'm used to seeing them for eye-watering prices in rough condition on eBay.

      -

      Next year

      I think VCF Midwest has outgrown its venue. It was a record attendance for the second year in a row. Things were awfully cramped.

      +

      Next year

      I think VCF Midwest has outgrown its venue. It was a record attendance for the second year in a row. Things were awfully cramped.

      I had an idea for more attendee engagement that I'd love if someone stole for next year. I was following the #vcfmw hashtag on Mastodon to see other people's pictures of cool stuff. Why not encourage attendees to use the hashtag with a little signage and hook up a vintage computer to a large monitor or a projector and display people's messages?

      -

      DOStodon on a Pentium would be a good choice since it has some image support. There's also a FujiNet Mastodon app that works on the big 3 micros: Apple ][, C64, and Atari 8-bits.

      +

      DOStodon on a Pentium would be a good choice since it has some image support. There's also a FujiNet Mastodon app that works on the big 3 micros: Apple ][, C64, and Atari 8-bits.

      I hope to see you there next year!

      -
      - - - - - + + + + + + + + + + + + + + +
      + -
    - + + + + +
    + \ No newline at end of file