ruby on rails

Use Pundit Policies to manage permitted parameters, defaults and per action

To manage permitted params you can add permitted_attributes and permitted_attributes_for_#{action} in your policies e.g.

class CommentPolicy < ApplicationPolicy
  def permitted_attributes
    %i[body]
  end

  def permitted_attributes_for_create
    %i[body application_id]
  end
end

Then in your controller

class CommentController
  def create
    @comment = Comment.create(comment_attributes) # { body: 'body', application_id: 1 }
  end
 
  def update
    @comment = Comment.find(params[:id])
    @comment.update(comment_attributes) # { body: 'new body' }
  end
 
  private def comment_attributes
    permitted_attributes(Comment)
  end
end
11
Joe

Use `requestSubmit` for form submits with Turbo

- `HTMLFormElement.submit()`: Does NOT trigger `submit` event
- `HTMLFormElement.requestSubmit()`: Triggers `submit` event

`Turbo` listens to submit events on form. 
Hence, always use `requestSubmit` to submit  a form using JS.
10
Abhi

Ignore bundled gems for Rubocop and ESLint in GitHub Actions

Rails projects will often install gems into the vendor directory in GitHub actions so we can cache them for the consecutive runs.

If you use code linters in the action you need to exclude the `vendor/bundle/*` to prevent false positives

For ESLint this can be configured through ignorePatterns
// ./.eslintrc.json
{
  "ignorePatterns": [
    "vendor/bundle/*"
  ]
}

For Rubocop you can exclude through the AllCops config
# ./.rubocop.yml
AllCops:
  Exclude:
    - 'vendor/**/*'

Importance of Ignoring Bundled Gems in Code Linters


In software development, maintaining high code quality is essential. Code linters are a tool to help achieve this by performing static code analysis, which can detect potential errors and enforce a consistent coding style. However, when using linters in a continuous integration environment, such as GitHub Actions, it's crucial to configure them correctly to avoid false positives.

Bundled gems, often stored in the vendor directory, are external code dependencies that can be flagged by code linters if not properly excluded. By ignoring these directories, you can ensure that the linter focuses on your source code rather than external libraries, thus maintaining the integrity of your code analysis.

Configuring Linters to Exclude Bundled Gems


For JavaScript projects using ESLint, you can configure the linter to ignore the vendor/bundle/* directory through the ignorePatterns setting in your .eslintrc.json file. This ensures that only your code is checked, improving code quality without unnecessary distractions.

// ./.eslintrc.json
{
  "ignorePatterns": [
    "vendor/bundle/*"
  ]
}

Similarly, for Ruby projects using Rubocop, you can exclude the vendor directory by adding an Exclude rule under AllCops in your .rubocop.yml configuration file. This helps focus the static code analysis on your actual source code.

# ./.rubocop.yml
AllCops:
  Exclude:
    - 'vendor/**/*'

By configuring your code linters to exclude bundled gems, you can maintain a high standard of code quality while avoiding false positives in your static code analysis.

#RUBY-ON-RAILS
15
Joe

Rails 6, Ruby 3, Bootstrap 5 and Hotwire

Extending the blogpost from Bootrails with Hotwire:
All the fancy new things (as of when I'm writing this TIL):

$ ruby --version
3.0.1
$ rails --version
Rails 6.1.3.1
$ rails new MyApp --skip-bundle --skip-coffee --database=postgresql
$ cd MyApp
# Add Hotwire-rails to Gemfile
$ echo "\ngem 'hotwire-rails'" >> Gemfile
$ bundle
$ ./bin/rails webpacker:install
# lets keep javascript and stylesheets in a app/frontend folder
$ mv app/javascript app/frontend
# add bootstrap dependencies
$ yarn add bootstrap@next
# when prompted, choose the latest version here:
$ yarn add @popperjs/core@next

Edit `./webpacker.yml` and modify source_path:
# Inside webpacker.yml
default: &default
  source_path: app/frontend # Change here

Add files to load bootstrap:
$ mkdir app/frontend/js && touch app/frontend/js/bootstrap_js_files.js
$ touch app/frontend/packs/application.scss
# Warning, this removes application.css (which we replace with application.scss)
$ touch app/assets/stylesheets/.keep && rm app/assets/stylesheets/application.css

// inside app/frontend/js/bootstrap_js_files.js
// enable plugins as you require them  

// import 'bootstrap/js/src/alert'  
// import 'bootstrap/js/src/button'  
// import 'bootstrap/js/src/carousel'  
import 'bootstrap/js/src/collapse'  
import 'bootstrap/js/src/dropdown'  
// import 'bootstrap/js/src/modal'  
// import 'bootstrap/js/src/popover'  
import 'bootstrap/js/src/scrollspy'  
// import 'bootstrap/js/src/tab'  
// import 'bootstrap/js/src/toast'  
// import 'bootstrap/js/src/tooltip'

// inside app/frontend/packs/application.js  

// Add this line before import Rails …
import '../js/bootstrap_js_files.js'  

// inside app/frontend/packs/application.scss  

// Import Bootstrap v5  
@import "~bootstrap/scss/bootstrap";

Setup Hotwire
# create ./bin/bundle; currently required for the Remove Turbolinks task by Stimulus
# see: https://github.com/hotwired/stimulus-rails/issues/55
$ bundle binstubs bundler
$ ./bin/rails hotwire:install

Modify `app/views/layouts/application.html.erb`:
<!DOCTYPE html>
<html>
  <head>
    <title>MyApp</title>
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>

    <!-- Warning !! ensure that "stylesheet_pack_tag" is used, line below; also change turbolinks to turbo for hotwire -->
    <%= stylesheet_pack_tag 'application', media: 'all', 'data-turbo-track': 'reload' %>
    <%= javascript_pack_tag 'application', 'data-turbo-track': 'reload' %>
  </head>

  <body>
    <%= yield %>
  </body>
</html>

Check if everything compiles:
$ ./bin/rails assets:clobber
# You can probably ignore the warnings :)
$ ./bin/rails webpacker:compile
14
Const

Rails Cache-Control

For enabling setting cache-control headers of static assets and using a CDN on Heroku:

config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present?
config.public_file_server.headers = {
  'Cache-Control' => 'public, max-age=31536000',
  # You need to enable this (or set it to your domain) for fonts to work
  'Access-Control-Allow-Origin' => '*'
}


if ENV['ASSET_HOST'].present?
  config.action_controller.asset_host = ENV['ASSET_HOST'] # 'https://cdn.example.com'
end

7
Const

Unique ID for DOM in Rails

Rails has many great view helpers, one of which is dom_id:
dom_id(Post.find(12))       # => "post_23"
dom_id(Post.new)            # => "new_post"

You can also provide a prefix:
dom_id(Post.find(23), :edit) # => "edit_post_23"
dom_id(Post.new, :custom)    # => "custom_post"
5
Const

Fix input on Heroku's rails console

If you end up pasting lots of lines into a rails console on Heroku (or else where), you might get issues with the lines "bleeding" into each other.

Next time this happens, try disabling multiline:

$ heroku run 'bundle exec rails console -- --nomultiline'
6
Const

PostgreSQL UPDATE Join

This query shows how to use the PostgreSQL UPDATE join syntax to update data in a table based on values in another table. for example;

updating subscription_missions.giftbox_type with rewards.gift_box_type through mission_rewards table (tested on 300k records, crazy level fast)

  UPDATE
        subscription_missions sm
  SET
        giftbox_type = r.gift_box_type
  FROM
        mission_rewards mr,
        rewards r
  WHERE
        sm.giftbox_type IS NULL
        AND sm.id = mr.subscription_mission_id
        AND mr.reward_id = r.id;



1
Ali

List your model names

$ RUBYOPT='-W0' rails runner 'Rails.application.eager_load!; puts ApplicationRecord.descendants.collect { |type| type.name }'


NOTE: RUBYOPT=-W0 avoids verbosity when running rails command which suppresses warnnings.

1
Ali

Postgres Functions & Non-sargable Queries

Using postgres functions inside a where clause can make a query non-sargable.

Database Structure

tracks has_many artists

Non-Sargable Query

Using a LOWER function prevents DBMS engine from using indexes.

Track.joins(:artists).where('LOWER(tracks.display_name) LIKE ?', "eric clapton%").explain

Gather  (cost=1000.85..56714.32 width=4061)
   Workers Planned: 2
   ->  Nested Loop  (cost=0.85..55713.62 rows=3 width=4061)
         ->  Nested Loop  (cost=0.42..55708.37 rows=3 width=4069)
               ->  Parallel Seq Scan on tracks  (cost=0.00..55365.17 rows=41 width=4061)
                     Filter: (lower((display_name)::text) ~~* 'eric clapton%'::text)
               ->  Index Scan using index_artist_relations_on_artist_item_type_and_artist_item_id on artist_relations  (cost=0.42..8.36 rows=1 width=16)
                     Index Cond: (((artist_item_type)::text = 'Track'::text) AND (artist_item_id = tracks.id))
         ->  Index Only Scan using idx_35952_primary on artists  (cost=0.43..1.75 rows=1 width=8)
               Index Cond: (id = artist_relations.artist_id)


Sargable Query

Removing LOWER function allows DBMS engine to use indexes, resulting in faster execution.

Track.joins(:artists).where('tracks.display_name ILIKE ?', "eric clapton%").explain

Nested Loop  (cost=1497.60..2695.43 width=4061)
   ->  Nested Loop  (cost=1497.17..2684.94 rows=6 width=4069)
         ->  Bitmap Heap Scan on tracks  (cost=1496.75..1873.05 rows=97 width=4061)
               Recheck Cond: ((display_name)::text ~~* 'eric clapton%'::text)
               ->  Bitmap Index Scan on index_tracks_on_display_name  (cost=0.00..1496.73 rows=97 width=0)
                     Index Cond: ((display_name)::text ~~* 'eric clapton%'::text)
         ->  Index Scan using index_artist_relations_on_artist_item_type_and_artist_item_id on artist_relations  (cost=0.42..8.36 rows=1 width=16)
               Index Cond: (((artist_item_type)::text = 'Track'::text) AND (artist_item_id = tracks.id))
   ->  Index Only Scan using idx_35952_primary on artists  (cost=0.43..1.75 rows=1 width=8)
         Index Cond: (id = artist_relations.artist_id)


Sargable and Non-Sargable


A sargable query is one that can use an index to speed up the query. A sargable query allows the DBMS engine to do an index seek, which is much faster than scanning the whole table. A non-sargable query can’t use an index, so it’s slower. The term “sargable” comes from “Search ARGument ABLE,” meaning the DBMS engine can use an index to optimize the query. Understanding the difference between sargable and non-sargable is key to optimizing your database.

Non-Sargable Queries


Non-sargable queries can have a big impact on query performance especially on large datasets. When a query is non-sargable, the DBMS engine has to do a full table scan or an index scan, both of which are heavy operations. This can result to slower execution time, higher CPU usage and overall poor system performance. Finding and optimizing non-sargable queries is key to improve your database’s efficiency and responsiveness.

Functions in the WHERE Clause


Using functions in the WHERE clause can make a query non-sargable. This happens because the DBMS engine can’t use an index to optimize the query when a function is applied to a column in the WHERE clause. For example, consider the query SELECT  FROM table WHERE UPPER(column) = 'VALUE'. This is non-sargable because the UPPER function is applied to the column, so the index can’t be used. To make this query sargable, you can remove the function and rewrite the query as SELECT FROM table WHERE column = 'VALUE'. Then the DBMS engine can use an index and the query will be faster.

Indexes and Query Optimization


Indexes are key to query optimization because they allow the DBMS engine to find the data quickly, hence speeding up the query. But an index can only be used if the query is sargable. If the query is non-sargable, the index can’t be used and the query will be slower. So create indexes on columns used in the WHERE clause and make sure the queries are sargable. This will improve your database’s performance big time.

Converting Non-Sargable Predicates


Converting non-sargable predicates to sargable is a query optimization step. A non-sargable predicate can be converted to sargable by rewriting the query to remove any functions or operations that prevents the use of an index. For example the query SELECT  FROM table WHERE YEAR(date) = 2022 is non-sargable because the YEAR function is applied to the date column. To make this query sargable you can rewrite it as SELECT FROM table WHERE date >= '2022-01-01' AND date < '2023-01-01'. This rewritten query is sargable because the DBMS engine can use an index on the date column to optimize the query and will be faster.

1
Abhi

How to find god objects in your project (Rails)

Go to your project root and cd into app/models (using CLI obviously) then run wc -lw * | sort -u

$ wc -lw * | sort -u
103     186 deal_broker.rb
116     313 physical_sim_report.rb
126     229 coupon_code.rb
126     260 subscription_mission.rb
129     284 payment.rb
145     290 deal_value.rb
147     255 single_use_coupon.rb
166     344 sim_card.rb
166     406 coupon_campaign.rb
225     451 order_summary.rb
260     534 deal.rb
308     783 package.rb
443    1031 user.rb
728    1744 subscription.rb
7262   15841 total


So now you can see Rocky is all about User ,Subscription and Package.

Note: First column number of lines and the second column number of words.

Tag rails logs with useful information

Just learned about tagged logging in rails. Did you know you can tag a log by using

logger.tagged('Your Tag') { logger.info('Message') }


config/application.rb


You can also do it globally with

# config/application.rb
config.log_tags = [:method_on_request_object, lambda { |request| request.method.modifier_method }]


config/application.rb


Add client Device OS and OS Version information to logs (requires clients to send X-OS and X-OS-Version headers)

# config/application.rb
config.log_tags = [ :request_id, lambda { |request| "#{request.headers['HTTP_X_OS']} #{request.headers['HTTP_X_OS_VERSION']}" } ]


For production environments, specific settings can be added in the config environments production.rb file to optimize logging and performance.

1. Introduction to Log Tags


Log tags are a powerful feature in Rails that allows you to add custom information to your logs, making it easier to debug and understand your application’s behavior. By using log tags, you can categorize related log messages and make your logs more readable and useful. In this section, we will introduce the concept of log tags and explain how to use them in your Rails application.

2. Configuring Rails Components


Configuring Rails components is an essential part of setting up your application. In this section, we will discuss how to configure various Rails components, including the application, environment, and middleware. We will also cover how to use the config method to set up custom configurations for your application.

3. Customizing Log Tags


Customizing log tags allows you to add specific information to your logs that is relevant to your application. In this section, we will discuss how to customize log tags using the log_tags method. We will also cover how to use Procs and objects that respond to to_s to add dynamic information to your logs.

4. Log Tagging Examples


Log tagging is a versatile feature that can be used in various ways to improve your logs. In this section, we will provide examples of how to use log tags in different scenarios, such as logging requests to external APIs, background jobs, and user information. We will also cover how to use log tags to debug multi-tenant applications.

5. Best Practices for Log Tags


Using log tags effectively requires some best practices to ensure that your logs are readable and useful. In this section, we will discuss best practices for using log tags, including how to keep your logs concise, how to use meaningful tag names, and how to avoid over-tagging. We will also cover how to use log tags in conjunction with other logging tools and techniques to get the most out of your logs.
828
Joe

Limited has_many associations

# app/models/user.rb
has_one :four_car_garage
has_many :cars
validates :cars, length: { maximum: 4 }


Joe

Create idempotent migrations

When you want to write destructive migrations please use if_exists: true when you’re trying to remove a table and check for a table existence when you want to add or remove a column ActiveRecord::Base.connection.table_exists?

Case 1:

def up
  drop_table :kittens, if_exists: true
end


Case 2:

def up
  return unless ActiveRecord::Base.connection.table_exists?(:kittens)
  add_column :kittens, :kind, :integer
end


Ali

If you want to override previously set order

If you want to override previously set order (even through default_scope), use reorder() instead.

E.g.

User.order('id ASC').reorder('name DESC')


would ignore ordering by id completely

1
Ali

Attaching file to ActiveStorage without HTTP

If you want to attach a file you generated on disk or downloaded from a user-submitted URL, you can do it like this.

@model.image.attach(
    io: File.open('/path/to/file'),
  filename: 'file.pdf',
  # content type is optional
  content_type: 'application/pdf'
)


3
Tino

Add new types to Rails 5 attributes API

The Rails 5 attributes API allows us to build form objects very easily.

I had previously been using an after_initialize callback to downcase emails, with the following code I can define an :email date type and values are automatically cast when set.

This has the advantage of casting subsequent calls to set the email value.

class EmailType < ActiveModel::Type::String
  def cast(value)
    return super unless value.is_a?(String)
    super(value.downcase)
  end
end

ActiveModel::Type.register(:email, EmailType)


class Foo
  include ActiveModel::Model
  include ActiveModel::Attributes

  attribute :email, :email
end


> Foo.new(email: 'EMAIL@MAIL.com').email
=> 'email@mail.com'


1
Joe

Scope or scope in Rails

# app/models/user.rb
...
enum role: { user: 0, author: 1, admin: 2, robot: 3 }

scope :human, -> { user.or(author) }
scope :terminator, -> { admin.or(robot) }


Produces

User.human.to_sql
#=> "SELECT \"users\".* FROM \"users\" WHERE (\"users\".\"role\" = 0 OR \"users\".\"role\" = 1)"


HTML forms that submit to another action

I've known for a long time that you can submit values with submit buttons so you can track which button was used to submit the form e.g.

language-html

However, today I needed to actually submit the form to a completely differnt action. Turns out you can do this with formaction attributes

language-html

Show TODO and other notes in your Rails app

Did you know you can show all TODOs/OPTIMIZE/FIXME in your rails app with rails notes?

$ rails notes
app/helpers/users_helper.rb:
  * [10] [TODO] Use ActiveSupport extension methods for Date/Time

app/services/slack.rb:
  * [20] [OPTIMIZE] Replace library with core Net::HTTP



You can even focus with notes:todo etc.:

$ rails notes:todo
app/helpers/users_helper.rb:
  * [10] [TODO] Use ActiveSupport extension methods for Date/Time


1.07 Thousand
Const

Create a Hash from an Enumerable (Rails 6.0)

New method allowing you to create a Hash from an Enumerable:

%w(driver owner drivy).index_with(nil)
# => { 'driver' => nil, 'owner' => nil, 'drivy' => nil }

%w(driver owner drivy).index_with { |role| delta_amount_for(role) }
# => { 'driver' => '...', 'owner' => '...', 'drivy' => '...' }


Use minItems/maxItems in collection JSON schemas

To make your collection JSON schemas more reilable use minItems and maxItems so that you can trust your API.

{
  "type": "array",
  "minItems": 1,
  "items": {
    "$ref": "subscription_notification.json"
  }
}



instead of

{
  "type": "array",
  "items": {
    "$ref": "subscription_notification.json"
  }
}



If your API returns [] empty array that last one would pass if you make an assertion in your specs.

expect(response).to match_response_schema(:subscription_notifications)



But not the first one

The property '#/' did not contain a minimum number of items 1 in schema


Understanding JSON Schema Arrays


JSON Schema arrays are essential for defining the structure and constraints of arrays in JSON data. An array in JSON is an ordered collection of values, and a JSON Schema array is used to specify the characteristics and restrictions of these arrays. By using a JSON Schema array, you can validate the length, contents, and uniqueness of an array, ensuring that the data adheres to the expected format. This is particularly useful when you need to enforce specific rules on the data structure, such as requiring a certain number of items or ensuring that all items are of a particular type.

Validation Keywords for Arrays


JSON Schema provides several validation keywords that can be used to constrain arrays. These keywords include:

  • items: Specifies the schema for each item in the array.

  • additionalItems: Specifies the schema for any additional items in the array beyond the first items schema.

  • minItems and maxItems: Specify the minimum and maximum number of items in the array.

  • uniqueItems: Specifies whether the items in the array must be unique.

  • contains: Specifies a schema that at least one item in the array must match.

  • minContains and maxContains: Specify the minimum and maximum number of items in the array that must match the contains schema.

These keywords allow you to define precise constraints on the array, ensuring that the data meets the required criteria.

Constraining Array Length


JSON Schema provides several ways to constrain the length of an array. The minItems and maxItems keywords can be used to specify the minimum and maximum number of items in the array. For example: { "type": "array", "minItems": 2, "maxItems": 5 } This schema specifies that the array must have at least 2 items and at most 5 items. By using these keywords, you can ensure that the array length falls within the desired range, preventing issues with too few or too many items.

Best Practices for Array Validation


When validating arrays with JSON Schema, it’s a good idea to use a combination of validation keywords to ensure that the array meets the required constraints. For example, you might use items to specify the schema for each item in the array, and minItems and maxItems to specify the minimum and maximum number of items in the array.

It’s also a good idea to use the uniqueItems keyword to ensure that the items in the array are unique, if that’s a requirement for your data. By combining these keywords, you can create a robust schema that thoroughly validates the array structure and contents.

Example Use Cases


Here are a few example use cases for JSON Schema arrays:

  • Validating a list of user IDs: You might use a schema like this to validate a list of user IDs:

{ "type": "array", 
"items": {"type": "integer"}, 
"minItems": 1, "maxItems": 10 } 

  • Validating a list of product names: You might use a schema like this to validate a list of product names:

{ "type": "array", "items": {"type": "string"}, "minItems": 1, "maxItems": 5, "uniqueItems": true } 

  • Validating a list of addresses: You might use a schema like this to validate a list of addresses:

{ "type": "array", "items": { "type": "object", "properties": { "street": {"type": "string"}, "city": {"type": "string"}, "state": {"type": "string"}, "zip": {"type": "string"} }, "required": ["street", "city", "state", "zip"] }, "minItems": 1, "maxItems": 10 } 

These examples illustrate how to apply JSON Schema arrays to different types of data, ensuring that the arrays meet the specified constraints and requirements.



Ali