Today I Learned

hashrocket A Hashrocket project

Casting Associations in Phoenix

If you want to cast changes on a struct's associations, you can use the changeset function cast_assoc/3. This allows you to make changes to a parent struct and its associations at the same time.

Example

Lets say you have a struct called Artist and an associated schema called Album. The schema for the Album looks something like this:

def module MyApp.Music.Album do
 use Ecto.Schema
 alias MyApp.Music.Artist

  schema "albums" do
    field :title, :string
    
    belongs_to :artist, Artist
  end

  def changeset(artist, params \\ %{}) do
    artist
    |> cast(params, [:title, :artist_id])
    |> validate_required([:title])
  end
end

and the schema for artist looks like this:

def module MyApp.Music.Artist do
 use Ecto.Schema
 alias MyApp.Music.Album

  schema "albums" do
    field :name, :string
    
    has_many :albums, Album
  end
end

Now, if you wanted to build a changeset for the Artist where you can change the artist's name and the title field on an associated album:

def changeset(artist, params \\ %{}) do
    artist
    |> cast(params, [:name])
    |> cast_assoc(:albums, with: &MyApp.Album.changeset/2)
    |> validate_required([:name])
end

Bulk Delete Records in Rails with `delete_by`

I'm sure you're familiar with the Active Record method delete_all . But I recently learned of another method that can used to shorthand this method-call on a where statement.

You see - delete_by(...) is the shorthand for Model.where(...).delete_all

Model.where(foo: true, bar: false).delete_all

# vs.

Model.delete_by(foo: true, bar: false)

Similarly to delete_all, you will need to be careful with delete_by as it creates a single delete statement for the database and callbacks are not called.

Bonus round -> destroy_by is a similar shorthand for Model.where(...).destroy

https://api.rubyonrails.org/classes/ActiveRecord/Relation.html#method-i-delete_by

Upload Active Storage Attachments to Specific Path

If you've ever wanted to organize your bucket while using Active Storage, it looks like this is now possible as of Rails 6.1

By passing the key argument to #attach, you can specify the location of the file within the bucket -

class Invoice < ApplicationRecord
  has_one_attached :document
end

invoice = Invoice.create
invoice.document.attach(
  key: "invoices/invoice_1_20230505.pdf",
  io: File.read("invoice_1.pdf")
)

https://github.com/rails/rails/commit/4dba136c83cc808282625c0d5b195ce5e0bbaa68 https://github.com/rails/rails/issues/32790

Do not serve SVG images in email content

Turns out that even with modern email we cannot rely on SVG file support.

The Google image proxy that every image gets served by when mail is processed through Gmail will not serve your SVG content.

This is a long standing issue and apparently will not be fixed.

You can possibly use inline SVG or a failover via the image tags src & srcset attributes, however, just converting to PNG/JPG will end up being the least problematic.

Easily connect to postgres via proxy with service

A client has their RDS postgresql databases locked down to only allow connection from their EC2 instances. I found an easy way to connect with my local clients.

Add a local forward to ssh config

In ~/.ssh/config add something like

Host prod.client
  User ubuntu
  Hostname prod-ec2-instance.example.com
  LocalForward localhost:5433 rds-gibberish.us-west-1.rds.amazonaws.com:5432
  IdentityFile ~/production-ec2-key.pem

Add the credentials to pg service

In ~/.pg_service.conf save the user, database name, and password

[client-prod]
host=localhost
port=5433
user=rds-user
dbname=client-production
password=blablabla

Now you can start your ssh tunnel in 1 terminal:

ssh prod.client

And connect with any postgres client tool (pg_dump, psql, etc.) in another:

psql service=client-prod

warning

Well you just saved a production password to a plain text file, and now you can easily connect and muck things up in production. Make sure your machine is secure and be careful and stuff.

You can turn arrays into sentences!

There's a neat little method Array#to_sentence that turns your array into a string in sentence format.

It's pretty self-explanatory, so here are some examples:

superheroes = ["Spider-Man", "Batman", "Iron man", "Wonder woman"]

superheroes.to_sentence
=> "Spider-Man, Batman, Iron man, and Wonder woman"

superheroes.to_sentence(
  words_connector: " or ", 
  last_word_connector: " or maybe even "
)
=> "Spider-Man or Batman or Iron man or maybe even Wonder woman"

With slight configuration, you can even have this work in different locales. Check it out in the docs!

Don't auto generate Gem documentation in Ruby

I always forget to disable generation of gem documenatation until I see it getting generated during install :-(

Do yourself a favor and create a .gemrc if you don't already have one and add:

gem: --no-document

Now all of your gem installs will be speedier and take up less space.

Some of you may remember --no-ri & --no-rdoc, however --no-document takes care of both.

View the gem documentation for more info

Android crash logs

You can get android crash logs with adb

adb logcat --buffer=crash 

03-30 07:53:59.221 23769 23769 E AndroidRuntime: FATAL EXCEPTION: main
03-30 07:53:59.221 23769 23769 E AndroidRuntime: Process: expo.modules.mymodule.example, PID: 23769
03-30 07:53:59.221 23769 23769 E AndroidRuntime: java.lang.RuntimeException: java.lang.reflect.InvocationTargetException
03-30 07:53:59.221 23769 23769 E AndroidRuntime: 	at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:448)
03-30 07:53:59.221 23769 23769 E AndroidRuntime: 	at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:810)
03-30 07:53:59.221 23769 23769 E AndroidRuntime: Caused by: java.lang.reflect.InvocationTargetE

Find meta information about Github's services

If you need info about github like what their current SSH fingerprints are or IP address ranges of their services... it turns out that much of that is readily available via the meta information api endpoint

Going through the docs makes it seem like you need to do things like pesky authentication but you can just go right to https://api.github.com/meta and you'll find most of the info you're looking for.

image

Remove SSH keys from known host file by IP address

Github had a situation which caused the need to potentially remove Github's keys from your known_hosts file.

This can be done easily with the SSH command:

ssh-keygen -R github.com

This is great... except when it isn't. I specifically ran into an issue where my connection was trying to utilize specific github IP addresses. Have no fear... it turns out that the same command can be utilized.

ssh-keygen -R 140.82.114.4

I actually had to do a few of them

ssh-keygen -R 140.82.113.3

etc...

You could potentially use github's meta information endpoint to find address ranges, but that's a problem for a more clever person.

Disable "Try it out" feature of Swagger UI

Swagger UI is a great way to view API documentation. It's even got a cool feature where you can excercise the api right from the documentation.

However that may not be wanted or necessary in every occasion. Turning it off wasn't as straightforward as I'd have expected.

The Swagger UI documentation shows a configuration option called tryItOutEnabled. This sounds promising and the description even states:

Controls whether the "Try it out" section should be enabled by default.

So using our knowledge on how to configure Swagger UI through rswag, we try:

Rswag::Ui.configure do |c|
  c.config_object["tryItOutEnabled"] = false
end

However we'll find that this doesn't do anythingย ๐Ÿ˜•

Later I found that this particular option sets whether or not the "Try it out" feature is open by default.

If we utilize the config option of supportedSubmitMethods and provide an empty array, we'll get something more usable.

Rswag::Ui.configure do |c|
  c.config_object["supportedSubmitMethods"] = []
end

Now the button will completely disappear!

Configure Swagger UI when using rubygem rswag

The swagger documentation gem rswag contains the library Swagger UI. This allows your generated documentation to be viewable from your webserver.

It's great out of the box but is also more configurable than the gem's documentation would lead you to believe.

You have direct access to things like authentication but anything deeper than that can be controlled via a configuation object.

Rswag::Ui.configure do |c|
  c.config_object["showExtensions"] = true
end

Utilize the configuration's config_object. It is just a hash that you can set with keys that match available options found in Swagger UI's configuration docs

Create a Date object for a specific day

Say you have some date-specific functionality, and you want to test for a specific day of the week.

Date#commercial is what you're looking for! It will create a Date object for you based on the year,week, and day that you give it.

require 'date'

# Wednesday (3) of week 1 of year 2023
Date.commercial(2023, 1, 3)

In Rails we can take this a step further, for example, to get Friday of this week:

Date.commercial(Date.current.year, Date.current.cweek, 5)

In your testing you can simply make use of the Rails TimeHelpers to travel to that specific date you need:

next_friday = Date.commercial(Date.current.year, Date.current.cweek + 1, 5)

travel_to next_friday do
  # Friday specific code
end

Arbitrary Values with Tailwind

You can set custom values in tailwind, without customizing your theme like this:

<div class="w-[50rem]">...</div>

This bracket syntax works with basically anything in tailwind. For example, here's font size.

<p class="text-[14px]">...</p>

This is super helpful at times when you don't want to bother with tailwind's size classes.

Custom Flash Messages in Rails

Why Aren't My Flash Messages Working?

Turns out, there's 2 keys that Rails supports by default for flash messages. Those are alert and notice; you can use them like this in your controller:

redirect_to users_path, notice: "User created successfully"
# - or -
render :new, alert: "An error prevented the user from being saved"

But if your flash rendering code is generic enough, you might notice that explicitly setting a key/message on flash will work for values other than the defaults:

flash[:success] = "User created successfully"
redirect_to users_path

TIL Rails has a helper that will allow us to add our own custom flash messages - add_flash_type. You can use this an a controller (re: ApplicationController) to enable new flash message types. This will allow you to do the one liners in render and redirect calls:

class ApplicationController < ActionController::Base
  add_flash_type :my_flash_type
end

# ...

redirect_to users_path, my_flash_type: "User created successfully"
# - or -
render :new, my_flash_type: "An error prevented the user from being saved"

In addition, it will also add a variable in our views to retrieve this message:

<%= my_flash_type %>

https://api.rubyonrails.org/classes/ActionController/Flash/ClassMethods.html

Convert Changeset Into Schema With `apply_changes`

Often times, it's necessary to perform some intermediate operations on a Ecto changeset before saving it to the database. It can be easier to deal with the underlying schema. And you might need the entire schema rather than only the changes in the current changeset.

You can use the apply_changes/1 function to apply that changeset and receive schema with which you can perform operations. It's worth noting that the data is not validated when changes are applied, so care needs to be taken to ensure validity before an attempt to save that record.

iex> lead_schema = Lead.changeset(%{name: "Andrew", email: "Andrew.Vogel@hashrocket.com"})
                   |> apply_changes() 
%Lead{name: "Andrew", email: "Andrew.Vogel@hashrocket.com"}

iex> lead_schema |> downcase_email() |> do_some_other_processing()

https://hexdocs.pm/ecto/Ecto.Changeset.html#apply_changes/1

Pretty-print JSON in Ruby

When receiving a JSON payload it can, of course, be useful to see it in a more readable way. Turns out there is a built in utility in Ruby that can help with this.

Kernal#j & Kernal#jj

Utilizing the Kernal#j method:

foo = {name: "Matt", company: "Hashrocket"}

=> j foo
{"name": "Matt", "company": "Hashrocket"}

Utilizing the Kernal#jj method:

foo = {name: "Matt", company: "Hashrocket"}

=> jj foo
{
  "name": "Matt",
  "company": "Hashrocket"
}

Phoenix LiveView slot attributes

Today I learned how to define an attribute on a Phoenix LiveView slot using a do block:

slot :column
  attr :field, :atom
  attr :sortable, :boolean, default: false
end

def table(assigns) do
  ~H"""
  <table>
    <tr>
      <th :for={col <- @column}>
        <%= col.label %>
        <.sort_by :if={col.sortable} field={col.field}/>
      </th>
    </tr>
  </table>
  """
end

This way we can use our slots with attributes:

def render(assigns) do
  ~H"""
    <.table>
      <:colum field={:name} sortable>Name</:colum>
      <:colum field={:price}>Price</:colum>
      <:colum>Actions</:colum>
      ...
    </.table>
  """
end

Check this out

Curl `-T/--upload-file` in Faraday

I had a bit of trouble trying to find docs on how to do a curl --upload-file request with ruby. This flag is a special flag that tells curl to generate a PUT request with the body being the file(s) to upload to the remote server.

In my case, I wanted to upload a single file, and I accomplished this with the faraday and faraday-multipart gem:

require 'faraday'
require 'faraday-multipart'

conn = Faraday.new("https://example.com") do |f|
  f.request :multipart
end

upload_file = File.open("./path/to/image.jpg")

conn.put("/file-upload") do |req|
  req['Content-Type'] = 'image/jpeg'
  req['Content-Length'] = upload_file.size.to_s
  req.body = Faraday::Multipart::FilePart.new(
    upload_file,
    'image/jpeg'
  )
end

Part of the magic here is that you need to explicitly set the Content-Type and the Content-Length header.

https://github.com/lostisland/faraday-multipart

Set an input's cursor position with Javascript

If you need to hijack an input's cursor position, you can use .setSelectionRange(). This function takes two zero-based index arguments: a starting position and end position and makes makes a text-selection based on those start and end points. With this understanding, we can pass in the same index for both the start and end point to manually move the cursors position without making a selection. For example:

// move the cursor to the end of an input
inputElement.setSelectionRange(inputElement.value.length, inputElement.value.length)

//or move the cursor to a specific index
inputElement.setSelectionRange(3, 3)

//or make a selection with a different start/end
inputElement.setSelectionRange(3, 6)

Disable strong parameters in rails

โš ๏ธ Don't do thisYou should never do this. This was used for a non-public internal active admin application that needed to be updated. This app was not intended for public use. Please don't do this.
# config/environments/development.rb
require "active_support/core_ext/integer/time"

Rails.application.configure do
  config.action_controller.permit_all_parameters = true
end

Setting the url_options hash in the controller

When grabbing the url of an ActiveStorage record through any of these methods: ActiveStorage::Blob#url,ActiveStorage::Variant#url,ActiveStorage::Preview#url you may find yourself running into this error:

Cannot generate URL for <FILENAME> using Disk service, 
please set ActiveStorage::Current.url_options.

This can be resolved in the controller by utilizing a concern that ActiveStorage provides for us; ActiveStorage::SetCurrent.

It would look like this in your controller:

class MyController < ApplicationController
  include ActiveStorage::SetCurrent
  
  # The rest of your controller...
end

And now whenever you go to call #url on your ActiveStorage record, it will know where to generate the url from!


Including ActiveStorage::SetCurrent sets the ActiveStorage::Current.url_options hash to match the current request.

Control printable backgrounds in CSS

Have you created an amazing print version of your html page and when you go to print it out, it's not looking quite the same?

Hey those background colors are all missing!

Turns out that your user-agent (browser) is given control on how your elements should appear in different scenarios, like printing.

This is why when you print out a page with a background image, said image is generally not printed and you get a clean print.

Here the stamp style looks super elegant with our cornflowerblue background but when printing that we lose the look of the stamp due to the background being missing.

.stamp {
  background-color: cornflowerblue;
  border-radius: 5px;
  padding: 10px;
}

There is a way to control this though. The print-color-adjust css directive can be set so that your styles will do your printable bidding.

By default print-color-adjust is set to economy. I like to think of this as if we're saving money in unused ink.

With a small change to:

.stamp {
  background-color: cornflowerblue;
  border-radius: 5px;
  padding: 10px;
  print-color-adjust: exact;
}

We now get our stamp style that looks great in our browser to look great on a printed page as well.

!important classes in tailwind

If you need to add !important to one of your Tailwind classes, you simply need to add ! before the class name.

Here is a crude example:

Blue Background ๐Ÿ”ต

<div class="bg-red-500 bg-blue-500">
  <!-- Insert content here -->
</div>

Red Background ๐Ÿ”ด

<div class="!bg-red-500 bg-blue-500">
  <!-- Insert content here -->
</div>

When using this with responsive breakpoints, place it after the variant modifier.

Valid ๐Ÿ˜

<div class="md:!bg-red-500">
  <!-- Insert conent here -->
</div>

Invalid ๐Ÿ˜ข

<div class="!md:bg-red-500">
  <!-- Insert content here -->
</div>

Pattern Matching Args with Exact Values

In elixir there is a handy trick for pattern matching for exact values. Let's say we have a function head_match that takes two arguments, a string and a list, that checks if the string argument is the same as the head of the list. We could use pattern matching like this:

def head_match(head, [head, _tail]) do
  IO.puts("Thats the head of the List!")
end
#this will match if we called it like head_match("first_word", ["first_word", "second_word"])

See how we named the first argument head and used that same name as the list head? This means that the function will only match if those two values are the exact same rather than just matching the structure of the arguments.

Shorthand Elixir Anonymous Functions

Elixir has a really cool syntax for writing anonymous functions (unnamed functions). It goes like this:

# The '&' operator is used to define the function and its arguments
putter = &(IO.puts &1)
putter.("Today I learned")
#...> Today I learned
#...> :ok

&1 refers to the functions first argument; in this example, we used "Today I learned". You can use more arguments with &2, &3, etc... For example:

combiner = &(&1 <> &2)
combiner.("Foo", "Bar")
#...> "FooBar"

Also notice that we are not calling either of these functions like function(arg1, arg2), instead we are calling them like function.(arg1, arg2). We have to use . because we are not actually naming these functions, we are only assigning them a reference, hence "anonymous" functions.

Rails' .insert_all method is too naive

Rails requires a unique index in order to use the .insert_all methods. This requirement can make this method very brittle and unusable. If your conflict target is the table's primary key, this won't work unless you create a redundant index on the table for this method to match against. This creates an amazing amount of waste not only of storage space, but also performance. This method would allow so many more use cases if it simply let you describe the conflict you want to match against.

More advanced method:

class ApplicationRecord
  def self.bulk_insert(array_of_hashes, conflict_targets = Array(primary_key))
    columns = array_of_hashes.first.keys
    values = array_of_hashes.flat_map(&:values)
    rows = array_of_hashes.map do |f|
      "(#{columns.size.times.map { "?" }.join(", ")})"
    end.join(", ")

    sql = sanitize_sql_array([<<~SQL, *values])
      INSERT INTO "#{table_name}"
      (#{columns.map { |c| "\"#{c}\"" }.join(",")})
      VALUES #{rows}
      ON CONFLICT (#{conflict_targets.map { |c| "\"#{c}\"" }.join(", ")}) DO NOTHING
    SQL

    connection.execute(sql.squish)
  end
end

SQL it produces:

User.bulk_insert([{email: "a@example.com"}, {email: "b@example.com"}])
INSERT INTO "users" ("email") VALUES ('a@example.com'), ('b@example.com') ON CONFLICT ("id") DO NOTHING

This would then allow you to reference any conflict you like:

alter table users add unique (email);
User.bulk_insert(
  [{email: "a@example.com"}, {email: "b@example.com"}],
  %i[email]
)

Check if a form is valid

You can check if a form passes HTML validations with javascript using the checkValidity function:

<form name="login">
 <fieldset>
   <legend>Email</legend>
   <input type="email" name="email" required />
 </fieldset>

 <fieldset>
   <legend>Password</legend>
   <input type="password" name="password" required />
 </fieldset>

 <div style="margin-top: 1rem">
   <input type="submit" value="login" />
   <button type="button" id="validate">
    validate
   </button>
 </div>
</form>

<script>
  const button = document.querySelector("#validate")
  if (button) {
    button.addEventListener("click", function() {
      alert(document.forms.login.checkValidity() ? "Form is valid" : "INVALID");
    });
  }
</script>

The difference between %w and %W in Ruby

%w can construct space delimited word arrays like this

%w(my cool word array)
#=> ["my", "cool", "word", "array"]

%W works similarly, however it offers ways to interpolate with variables and escape special characters in the assignment, while %w does not.

street_name = 'Sesame Street'
%W(I live on #{street_name})
#=> ["I", "live", "on", "Sesame Street"]

Sort array of numbers

In javascript, the Array sort function will cast everything to a string. So when you have an array of numbers, you need to repetitively cast them to numbers:

[1080, 720, 480].sort()
=> [1080, 480, 720]

So you need have to write your own sort function (don't mess it up!)

[1080, 720, 480].sort((a, b) => Number(a) - Number(b))
=> [480, 720, 1080]




๐Ÿ‰

Limit Jest Test Coverage to Specific Files

Jest allows you to view test coverage when it runs.

But even if I only run tests for a specified file or files, the --coverage output will include all files.

$ jest src/directoryToTest --coverage

If your app is large, this can generate a lot of output and be difficult to parse. If I only care about the coverage for files in directoryToTest, I can filter the output of --coverage with --collectCoverageFrom=:

$ jest src/directoryToTest --coverage --collectCoverageFrom="src/directoryToTest/**"

docs

SVG using href must be served from same site url

We like to use SVG sprites

However, if you utilize a cdn or serve assets from an external source... you may run into some issues. Hopefully you find this and it saves you time that we lost figuring this out

According to MDN:

For security reasons, browsers may apply the same-origin policy on use elements and may refuse to load a cross-origin URL in the href attribute. There is currently no defined way to set a cross-origin policy for use elements.