RSSAmplifier

Blog

Stephen Lewis - Notes to self from a forgetful web developer

Standard issue nerd

stephenlewis.meRSS feed ↗176 posts

Latest posts

Small acts of defiance

I’ve been descending down the rabbit hole of self hosting recently. My motivation is the usual disquiet about Big Tech and data privacy, and a general annoyance that nobody sells software any more. Nothing particularly original or worthy of further discussion. The practicalities of such an endeavour are mostly banal and frequently frustrating, but the act is somehow perceived as one of defiance. I…

AI did not

AI did not delete your production database . You did. AI did not delete your production database, either . You did. AI did not bring down AWS, twice . You did. AI did not lie to your customers . You did. AI did not choose to discriminate . You did. AI did not continue to discriminate . You did. AI did not submit fake expert testimony . You did. AI did not invent fake news . You did. AI did not…

A different mindset

This morning I attempted to add a GitHub repo to one of my fastidiously curated lists. It didn’t work. I tried again with another repo. No dice. Annoyed by GitHub’s persistent flakiness, I added a task to my Todoist inbox: Migrate starred repos out of GitHub Six months ago , this would have languished on a “someday projects” list until the end of time. In this strange new reality, Claude built…

Make it right

Today I was reminded of the old Kent Beck maxim, “Make it run, make it right, make it fast”, and it struck me how well this applies to coding with AI. LLMs have become pretty good at making it run. But what does “right” mean in the context of this codebase? How fast is fast enough? These questions still require human judgement. This is what I find so bothersome about the hands-off approach to…

The thing that brought me joy

I’ve been using Neovim (and before that, Vim) for 20 years, and as such spend most of my day in the terminal. I like it there. As dumb as it sounds, it makes me happy. That doesn’t mean I’ve mastered Neovim though, much less the other tools that make the terminal so powerful. The esoteric superpowers of sed and awk still elude me. The dream of effortlessly piping text through a series of small,…

Move slowly and make things

All the cool kids are supposedly vibe coding entire web applications in a day, but I just end up with a mess that I absolutely hate. Maybe that’s my failing, or maybe the cool kids just don’t care as long as it works–for now. Whatever the reason, it doesn’t work for me, and that’s okay. The “MVP in a day” culture is toxic bullshit, and I want no part of it. It takes time to build good software,…

Using Eloquent subqueries to randomise grouped results

Grouping items in a database query and then selecting a random item from each group is surprisingly tricky. As with most things involving GROUP BY , it doesn’t work quite how you 1 might expect. Let’s dig in. The contrived scenario Imagine you’re building a movie recommendation app. The films are stored in a movies table, which looks like this: title genre The Exorcist Horror The Raid Action The…

PhoenixTest foot guns

Writing LiveView feature tests is an occasionally frustrating experience, not helped by the inconsistent, opaque, and generally horrible API of the official module . Thankfully, German Velasco, a man who knows a thing or two about testing LiveView , is here to save us all with his splendid PhoenixTest library . Using it has turned testing LiveViews from a miserable chore into a genuine pleasure.…

Weeknotes: Claude

One of those weeks where you get to Thursday and realise you’ve accomplished next to nothing. The post-covid slump is real. On the plus side I became a thought leader , and shipped the best thing I will ever work on. A career high For reasons unclear, Mrs Flinger wants to receive more iOS notifications. Specifically, she wants to receive a random quote from her collection at intervals throughout…

The complexity halo

After working on a complex problem, I tend to over-complicate the next one. I call this the “complexity halo”. If you’re used to things being difficult, you assume difficulty. If you’re used to seeing problems, you see problems. One helpful tactic is to ask specific questions about your current task. It only takes a moment and stops you from disappearing down the rabbit hole. Here are some to get…

Weeknotes: Covid, mostly

After three years of cheating death, I finally caught Covid. My primary concern this whole time was that I’d pass it on to my parents and inadvertently kill them. As such, it was only appropriate that I caught it from my mum. Thanks, Mum, love you. Given that I’ve spent most of the week doing some combination of coughing, sneezing, sleeping, and wheezing, there isn’t a whole lot to report. That…

Weeknotes: Working on Hap, and working on working

I’m trying out weeknotes as a way to keep track of all the little things that don’t warrant a blog post. This week has been a balance of working on Hap , and making a start on finding a new contract (hopefully Elixir). Working on Hap My current side project is Hap , a real-time monitoring tool for your most important business metrics, built with Phoenix and LiveView. Before this week, Hap was…

Organising Ecto schemas

In a Phoenix application, you typically organise your schemas by context 1 . In theory this makes sense. The schemas are responsible for data validation, and the other modules within the context are responsible for data access. In practise, there’s a problem: it’s difficult to tell which is which. Take the Accounts context, as generated by the mix phx.gen.auth command: myapp/ accounts/ user.ex…

Naming Phoenix context functions

In a Phoenix context we frequently need to create and update resources, using changesets. Each operation requires a function to get the changeset, and a function to perform the operation. If the operation in question is “create”, the standard approach is to name the changeset function create_changeset , but that’s just confusing; am I getting a changeset for the “create” operation, or am I…

How to bypass the Git pre-push hook

Git’s pre-push hooks are a great way to enforce code quality. They’re also a pain when you just want to push some in-progress work to a branch. The --no-verify flag allows you to bypass the pre-push hook when pushing. Use it responsibly. $ git push - -no-verify

Building Happy Stack: naming things

Going into this project, I assumed the biggest problem would be how to retrieve the status of each product. Some products provide a nice status API, others provide a not so nice RSS feed. Others still provide nothing whatsoever, beyond some poorly-structured HTML. Much to my surprise, retrieving the status information wasn’t particularly problematic. As ever, the real problem was naming things.…

Building Happy Stack: notifications

The plan has always been to launch Happy Stack with email notifications; that’s it. I have lots of other notification channels planned of course, but email is still the most universal. For the most part, this focus on a single notification channel has made my life a lot easier. It keeps the UI simple, and the development straightforward. There was one thing bothering me, though: was I making life…

Simplify imports with path mapping

Your TypeScript application uses relative imports. Every time you move a file, the imports break. You’ve configured path mapping , and now things break in an entirely new way. At first glance, TypeScript’s path mapping feature appears to solve the problem of relative imports. Unfortunately, it’s not quite that simple. Assume we have the following files. // tsconfig.json { " compilerOptions " : { "…

Check a date with Chai

You need to check a date using Chai. You also need to account for the test execution time. Compare the milliseconds using Chai’s closeTo method. const updatedAt = new Date ( ) // Some time later... expect ( updatedAt . getTime ( ) ) . to . be . closeTo ( Date . now ( ) , 100 ) The second closeTo parameter controls the delta ; the largest acceptable difference between the two values.

Verify a checksum

You need to validate the checksum of a file. Rather than Googling it for the hundredth time, you decide to write a note to self, in the hope it will stick this time. Use the shasum command, with the appropriate algorithm flag. // SHA1 $ shasum - a 1 file.txt // SHA256 $ shasum - a 256 file.txt // SHA512 $ shasum - a 512 file.txt The above commands output the checksum for manual verification. You…

MongoDB ObjectId timestamp

You need to record the creation time for each document in a MongoDB collection. Your first thought may be to store this information in a createdAt column, but there’s no need. The MongoDB-generated document ID includes an embedded timestamp. You can verify this by running the following command in the mongo shell. ObjectId ( ) . getTimestamp ( ) // ISODate("2020-07-28T16:48:35Z")

Specify a default line-height for each font-size in Tailwind CSS

Tailwind 1.3 added the ability to specify a line-height for each font-size in your config file. The following example sets the base font-size to 1rem , and the line-height to 1.5 . // tailwind.config.js module . exports = { theme : { fontSize : { base : [ ' 1rem ' , ' 1.5 ' ] , } , } , } Say goodbye to fiddling with leading-* classes in your markup.

Using TypeScript with Gatsby

Gatsby supports TypeScript out-the-box. Unfortunately, the official solution has several shortcomings which limit its usefulness. There is a better way. Rather than using Babel to compile our TypeScript, we can run Gatsby on ts-node . That gives us proper type-checking, along with TypeScript support in Gatsby’s configuration files. Install your dependencies Install your dependencies as follows. Be…

Nested optional chaining in JavaScript

You can use JavaScript’s optional chaining operator multiple times within a statement. That is useful if you need to access nested properties which may not exist. const people = [ { name : ' John ' , } , { name : ' Jack ' , favoriteFilm : { title : ' Goodfellas ' , } , } , { name : ' Jane ' , favoriteFilm : { title : ' Monsters, Inc. ' , tagline : ' We scare because we care. ' , } , } , ] Here we…

Custom Decap CMS previews in Gatsby

You’re using Decap CMS 1 to manage the content of your Gatsby-powered website. The default Decap CMS preview displays every field, including metadata. That probably isn’t what you want. Register a preview template The Gatsby Decap CMS plugin allows you to customise Decap CMS using a JavaScript module. In the example below, we tell Gatsby to use our decap.js module. // ./gatsby-config.js module .…

Rounding errors in JSON Schema validation

JSON Schema includes the multipleOf keyword . This is very useful for checking that a number is rounded to a specific number of decimal places. { " type " : " number " , " description " : " An integer " , " multipleOf " : 1.0 } Unfortunately, rounding errors in some languages mean acceptable data can fail validation. If you’re using JavaScript, the Ajv library has a solution to this problem: the…

CSS-only highlighter effect

You can apply a “highlighter” effect to text using background gradients. . highlight { background : linear-gradient ( 100 deg , rgba ( 255 , 221 , 64 , 0 ) 0.9 % , rgba ( 255 , 221 , 64 , 1 ) 2.4 % , rgba ( 255 , 221 , 64 , 0.5 ) 5.8 % , rgba ( 255 , 221 , 64 , 0.1 ) 93 % , rgba ( 255 , 221 , 64 , 0.7 ) 96 % , rgba ( 255 , 221 , 64 , 0 ) 98 % ) , linear-gradient ( 180 deg , rgba ( 255 , 221 , 64 ,…

“Module not found” error with Gatsby, Netlify CMS, and PostCSS

Your site uses Gatsby and Netlify CMS. Tailwind and PostCSS take care of the styling. You run gatsby develop , and encounter a cryptic error message. Module not found: Error: Can't resolve './tailwindcss/base' The problem is the plugin order in your gatsby-config.js file. The PostCSS plugin must appear before the Netlify CMS plugin. The correct order is as follows. module . exports = { plugins : […

Undocumented Node Migrate options

Migrate is a very useful migration framework for Node. Unfortunately, its documentation is lacking in places. Here are a couple of handy options not covered by the README. Load environment variables with dotenv The --env flag loads environment variables using dotenv . This is particularly useful for database migrations which depend on environment-specific credentials. migrate up - -env Ignore…

Execute a command with another user’s environment

Assuming you have the correct privileges , sudo -u lets you execute a command as another user. For example, the following command runs yarn install as the user jimbob : sudo - u jimbob yarn install There’s a catch though: the above command doesn’t inherit JimBob’s user environment. That is problematic if you depend on something in JimBob’s .bash_profile . The -i flag fixes this problem. sudo - iu…

Custom TypeScript type guards

Type guards let you provide TypeScript with more information about a variable’s type. Type guards are especially useful when working with a variable which may be undefined . function lower ( s : string | undefined ) : string { return s . toLowerCase ( ) } The above code won’t compile 1 , as TypeScript doesn’t know whether s is a string or undefined . We can fix this problem using a type guard .…

Chai assert error

Chai lets you assert that some code throws an error. The key is to pass the test subject to expect , not the test result. Here’s how to assert that the trickyCode function throws an error with the message “That was tricky”. expect ( trickyCode ) . to . throw ( ' That was tricky ' ) If trickyCode requires an argument, wrap it in another function. expect ( ( ) => trickyCode ( { veryTricky : true } )…

Selectively disable ESLint

You can disable ESLint for a single line, a block of code, or an entire file. Wherever possible, specify the rule or rules you wish to disable. Disable ESLint for a single line Use the eslint-disable-line and eslint-disable-next-line directives to disable linting for a specific line. const bad_string = ' nope ' // eslint-disable-line camelcase // eslint-disable-next-line no-array-constructor const…

The right way to compare arrays with Chai

Don’t use “deep equals” when comparing arrays with Chai; it depends on element order. Examine the array members instead. // Fails expect ( [ ' b ' , ' a ' ] ) . to . deep . equal ( [ ' a ' , ' b ' ] ) // Passes expect ( [ ' b ' , ' a ' ] ) . to . have . members ( [ ' a ' , ' b ' ] )

Query MongoDB by array field size

Imagine you have a MongoDB collection named repos . Each document contains an array field named languages . For example. { " url " : " https://github.com/denoland/deno " , " languages " : [ " TypeScript " , " Rust " , " Python " , " JavaScript " , " HTML " ] } Find documents with exactly three languages Use the $size operator to specify an exact array length. db . repos . find ( { languages : {…

Numeric environment variables and TypeScript

The TypeScript implementation of JavaScript’s standard, built-in objects can be surprisingly unforgiving. Two examples of this are the parseInt function, and the Number.isInteger method. In TypeScript, parseInt accepts a string; Number.isInteger accepts a number or NaN . Anything else causes a type error. This can cause problems when working with numbers stored in environment variables. Imagine…

Visual Studio Code “actions” shortcut

VS Code has the concept of code actions . When a code action is available, VS Code displays a small lightbulb icon nearby 💡. Clicking on the lightbulb displays a list of available actions. This mouse-based approach works, but it’s slow and cumbersome. Instead, navigate to the code in question, and use the keyboard shortcut CMD-. .

Compute the difference between two JavaScript arrays

Assume we have two JavaScript arrays, alpha and bravo . We need to determine which items appear in alpha , but not in bravo . We can achieve this by combining the Array.prototype.filter method with the Array.prototype.includes method. const alpha = [ ' a ' , ' b ' , ' c ' ] const bravo = [ ' b ' , ' c ' , ' d ' ] // ['a'] const charlie = alpha . filter ( item => ! bravo . includes ( item ) )

Transform objects in a Node stream

By default, a Node.js stream expects to operate on a Buffer or a Uint8Array . We can override this by telling the stream to use “object mode”. Consider the following array of TypeScript objects. interface Person { id : number name : string age : number } const data : Person [ ] = [ { id : 1 , name : ' John Doe ' , age : 32 } , { id : 2 , name : ' Jane Doe ' , age : 28 } , { id : 3 , name : ' Gabe…

Sort the output of git status

When working on a codebase with a lot of unstaged or untracked changes, the default git status output is unhelpful. The following snippet reorders the git status output. Added, modified, and deleted files appear at the bottom, right above the command prompt. git status - -short | sort - -ignore-leading-blanks - -ignore-case git status - s | sort - bf If you’re not interested in untracked files,…

Clear the terminal in macOS

Typing clear at a command prompt clears the screen. Simple enough. Unfortunately, it’s no help if you’re inside a REPL, or a long-running process. CMD-K to the rescue.

JavaScript logical operators

If you’re accustomed to PHP’s logical operators, JavaScript’s implementation can be confusing. PHP In PHP, the rules are straightforward: $a && $b returns true if both $a and $b are truthy $a || $b returns true if either $a or $b are truthy For example. $ a = true ; $ b = ' truthy ' ; echo ( $ a && $ b ) ; // true $ c = null ; $ d = true ; echo ( $ c && $ d ) ; // false $ e = ' truthy; $f = false;…

Avoid Array.prototype.push

Avoid Array.prototype.push . It modifies the array in place, which is asking for trouble. It also returns the array length, not the modified array, which is plain confusing. Instead, prefer the array spread syntax 1 . const original = [ ' cat ' , ' sat ' ] const modified = [ ... original , ' mat ' ] If you need to go old school, there’s Array.prototype.concat . const original = [ ' cat ' , ' sat '…

Search the file explorer in Visual Studio Code

The VS Code Explorer has a nifty trick up its sleeve. Focus the Explorer, start typing, and VS Code highlights or filters the files and folders. Sadly, when using the Vim extension , this handy feature no longer works. Or so I thought. In true Vim fashion, you need to type / to search. Command-Shift-E , to open and focus the Explorer / to start searching Settings The…

Sane URL validation in JSON Schema

JSON Schema is a very useful tool for validating API requests. Sadly it’s not immune to the quagmire of pedantry that is URL validation. Consider the following request payload. { " data " : { " callbackUrl " : " ... " } } You may be tempted to validate this payload against the following JSON Schema. { " $id " : " https://www.stephenlewis.me/nope.json " , " $schema " : "…

Log nested objects in Node

The default “recurse depth” of console.dir is 2. This is rarely what I want. Consider the following contrived example. const person = { fullName : { given : ' John ' , family : ' Doe ' , } , visited : [ { place : ' Paris ' , country : { label : ' France ' , code : ' FR ' , } , } , { place : ' Nha Trang ' , country : { label : ' Viet Nam ' , code : ' VN ' , } , } , ] , } By default, console.dir…

Generate an array of random data

Faker is a useful JavaScript library for generating dummy data. For example, faker.random.words(3) generates a string containing three random words. But what if you want to generate an array of random words? Helper function to the rescue. function makeArray < T > ( length : number , generator : ( ) => T ) : T [ ] { return Array . from ( { length } , generator ) } Usage: // Array containing 20…

Split a JavaScript array into chunks

There are countless blog posts detailing how to split a JavaScript array into chunks. However, many of these solutions fail with arrays containing hundreds of thousands of items. This is frequently because they use recursion. Recursive solutions are problematic because barely any JavaScript engines support tail call optimisation . To see this in action, run the following in Node: const chunk = (…

Generate a pseudo-random boolean

You can generate a pseudo-random boolean in JavaScript or PHP using a single line of code. No, you do not need to install yet another idiotic JavaScript package . In JavaScript: const random = Math . random ( ) <= 0 . 4999 And in PHP: $ random = ( mt_rand ( 0 , 1 ) === 0 ) If you need a cryptographically-secure random boolean in PHP, use random_int instead : $ secure = ( random_int ( 0 , 1 ) === 0…

Modelling API requests with Statecharts

Statecharts are a very useful tool for modelling complex workflows in your application. XState is a full-featured JavaScript library, for working with statecharts and finite state machines. Here’s how to model an API request in XState, using an invoked promise . It’s a little intimidating, but I walk through the important part below. import axios from ' axios ' import { Machine , interpret } from…