BigBinary Blog

We write about Ruby on Rails, React.js, React Native, remote work, open source, engineering and design.

Rails 7 adds Enumerable#maximum

This blog is part of our Rails 7 series.

Rails 7 adds support for Enumerable#maximum and Enumerable#minimum to easily calculate the maximum and minimum value from a collection of enumerable elements.

Before Rails 7, we could only achieve the same results with a combination of map & max or min functions over the enumerable collection.

1=> Item = Struct.new(:price)
2=> items = [Item.new(12), Item.new(8), Item.new(24)]
3
4=> items.map { |x| x.price }.max
5=> 24
6
7=> items.map { |x| x.price }.min
8=> 8

This is simplified with Rails 7's newly-introduced maximum and minimum methods.

1=> items.maximum(:price)
2=> 24
3
4=> items.minimum(:price)
5=> 8

These methods are available through Action Controller's fresh_when and stale? for convenience.

1# Before Rails 7
2
3def index
4  @items = Item.limit(20).to_a
5  fresh_when @items, last_modified: @items.pluck(:updated_at).max
6end
7
8# After Rails 7
9
10def index
11  @items = Item.limit(20).to_a
12  fresh_when(@items)
13end

The etag or last_modified header values will be properly set here based on the maximum value of the updated_at field.

Check out this pull request for more details.

Ashik Salman in Rails, Rails 7
February 23, 2021
Share

Prettier's Prose Wrap and eslint maximum-line-length error

If you go through the Prettier's Prose Wrap documentation, you can see that it provides 3 options, that is

  • "always" - Wrap prose if it exceeds the print width.
  • "never" - Do not wrap prose.
  • "preserve" - Wrap prose as-is. (First available in v1.9.0)

Now, this does not exactly give a clear idea on what each feature does, and it could be a little confusing especially between the "never" and "preserve" options.

In the below video we walk you through these options and tries to get you a clear idea on Prettier's Prose Wrap and its use-case.

Links to the pages mentioned in the above video

Mazahir B Haroon in JavaScript
February 9, 2021
Share

Rails 6.1 adds support for PostgreSQL interval data type

This blog is part of our Rails 6.1 series.

What is PostgreSQL Interval Data Type?

PostgreSQL Interval Data Type allows us to store a duration/period of time in years, months, days, hours, minutes, seconds, etc. It also allows us to perform arithmetic operations on that interval.

There are two input formats for interval data. These formats are used to write interval values.

1. Verbose format:

1  <quantity> <unit> [<quantity> <unit>...] [<direction>]
2
3  # Examples:
4  '2 years ago'
5  '12 hours 13 minutes ago'
6  '8 years 7 months 2 days 3 hours'
  • quantity can be any number.
  • unit can be any granular unit of time in plural or singular form like days/day, months/month, weeks/week, etc..
  • direction can be ago or an empty string.

2. ISO 8601 formats:

1P <quantity> <unit> [ <quantity> <unit> ...] [ T [ <quantity> <unit> ...]]
  • ISO 8601 format always starts with P.
  • quantity and unit before T represents years, months, weeks and days of an interval.
  • quantity and unit after T represents the time-of-day unit.
1# Examples
2P1Y1M1D => interval of '1 year 1 month 1 day'
3P3Y1DT2H => interval of '3 years 1 day 2 hours'
4P5Y2MT3H2M => interval of '5 years 2 months 3 hours 2 minutes'
5# NOTE: If `M` appears before `T`,
6# it is month/months and if it appears after `T`, it signifies minute/minutes.
7
8OR
9
10P [ years-months-days ] [ T hours:minutes:seconds ]
11
12# Examples
13P0012-07-00T00:09:00 => interval of '12 years 7 months 9 minutes'
14P0000-10-00T10:00:00 => interval of '10 months 10 hours'

Arithmetic operations on interval

We can easily apply addition, subtraction and multiplication operations on interval data.

1'10 hours 10 minutes' + '30 minutes' => '10 hours 40 minutes'
2
3'10 hours 10 minutes' - '10 minutes' => '10 hours'
4
560 * '10 minute' => '10 hours'

Before Rails 6.1

PostgreSQL interval data type can be used in Rails but Active Record treats interval as a string. In order to convert it to an ActiveSupport::Duration object, we have to manually alter the IntervalStyle of the database to iso_8601 and then parse it as shown below:

1execute "ALTER DATABASE <our_database_name> SET IntervalStyle = 'iso_8601'"
2
3ActiveSupport::Duration.parse(the_iso_8601_formatted_string)

Rails 6.1

Rails 6.1 adds built-in support for the PostgreSQL interval data type. It automatically converts interval to an ActiveSupport::Duration object when fetched from a database. When a record containing the interval field is saved, it is serialized to an ISO 8601 formatted duration string.

The following example illustrates how it can be used now:

1# db/migrate/20201109111850_create_seminars.rb
2
3class CreateSeminars < ActiveRecord::Migration[6.1]
4  def change
5    create_table :seminars do |t|
6      t.string :name
7      t.interval :duration
8      t.timestamps
9    end
10  end
11end
12
13# app/models/seminar.rb
14class Seminar < ApplicationRecord
15  attribute :duration, :interval
16end
17
18>> seminar = Seminar.create!(name: 'RubyConf', duration: 5.days)
19>> seminar
20=> #<Event id: 1, name: "RubyConf", duration: 5 days, created_at: ...>
21
22>> seminar.duration
23=> 5 days
24
25>> seminar.duration.class
26=> ActiveSupport::Duration
27
28>> seminar.duration.iso8601
29=> "P5D"
30
31# ISO 8601 strings can also be provided as interval's value
32>> seminar = Seminar.create!(name: 'GopherConIndia', duration: 'P5DT7H6S')
33>> seminar
34=> #<Event id: 2, name: "GopherConIndia", duration: 5 days, 7 hours, and 6 seconds, created_at: ...>
35
36# Invalid values to interval are written as NULL in the database.
37>> seminar = Seminar.create!(name: 'JSConf', duration: '3 days')
38>> seminar
39=> #<Event id: 3, name: "JSConf", duration: nil, created_at: ...>
40

If we want to keep the old behaviour where interval is treated as a string, we need to add the following in the model.

1# app/models/seminar.rb
2class Seminar < ApplicationRecord
3  attribute :duration, :string
4end

If the attribute is not set in the model, it will throw the following deprecation warning.

1DEPRECATION WARNING: The behavior of the `:interval` type will be changing in Rails 6.2
2to return an `ActiveSupport::Duration` object. If you'd like to keep
3the old behavior, you can add this line to Event model:
4
5  attribute :duration, :string
6
7If you'd like the new behavior today, you can add this line:
8
9  attribute :duration, :interval

Check out the commit for more details.

Akhil Gautam in Rails, Rails 6.1
January 26, 2021
Share

Rails 6.1 allows per environment configuration support for Active Storage

This blog is part of our Rails 6.1 series.

Rails 6.1 allows environment-specific configuration files to set up Active Storage.

In development, the config/storage/development.yml file will take precedence over the config/storage.yml file. Similarly, in production, the config/storage/production.yml file will take precedence.

If an environment-specific configuration is not present, Rails will fall back to the configuration declared in config/storage.yml.

Why was it needed?

Before Rails 6.1, all storage services were defined in one file, each environment could set its preferred service in config.active_storage.service, and that service would be used for all attachments.

Now we can override the default application-wide storage service for any attachment, like this:

1class User < ApplicationModel
2  has_one_attached :avatar, service: :amazon_s3
3end

And we can declare a custom amazon_s3 service in the config/storage.yml file:

1amazon_s3:
2  service: S3
3  bucket: "..."
4  access_key_id: "..."
5  secret_access_key: "..."

But we are still using the same service for storing avatars in both production and development environments.

To use a separate service per environment, Rails allows the creation of configuration files for each.

How do we do that?

Let's change the service to something more generic in the User model:

1class User < ApplicationModel
2  has_one_attached :avatar, service: :store_avatars
3end

And add some environment configurations:

For production we'll add config/storage/production.yml:

1store_avatars:
2  service: S3
3  bucket: "..."
4  access_key_id: "..."
5  secret_access_key: "..."

And for development we'll add config/storage/development.yml:

1store_avatars:
2  service: Disk
3  root: <%= Rails.root.join("storage") %>

This will ensure that Rails will store the avatars differently per environment.

Check out the pull request to learn more.

Shashank in Rails, Rails 6.1
January 20, 2021
Share

Authorization in REST vs Postgraphile

Postgraphile is a great tool for making instant GraphQL from a PostgreSQL database. When I started working with Postgraphile, its authorization part felt a bit different compared to the REST based backends which I had worked with before. Here I will share some differences that I noted.

First, let's see Authentication vs Authorization.

Authentication is determining whether a user is logged in or not. Authorization is then deciding what the users has permission to do or see.

Comparing the implementation of a blog application using Postgraphile vs REST

Suppose we have to build a blog application with the below schema.

event delegation

Features of the blog application.
  • Display published blogs with is_published = true to all users.

  • Display unpublished blogs with is_published = false to its creator only.

REST Implementation

The REST implementation with JavaScript and sequelize can be like below.

REST implementation

The client requests the blogs using an endpoint, it also attaches the access token received from the authentication service.

1const getBlogs = () => requestData({
2    endpoint: `/api/blogs`,
3    accessToken: '***'
4});

The backend code in the server receives the request, finds the current logged in user from the access token, and requests the data based on the current logged in user from the database.

1const userEmail = findEmail(accessToken)
2const blogs = await models.Blogs.findAll({
3where: { [Op.or]:[
4    {creatorEmail: userEmail},
5    {isPublished: true}
6  ]},
7});
8
9res.send(blogs);

Here, the backend code finds the user’s email from the access token, then requests the database to give the list of blogs that have creatorEmail matching to the current user's email or the field isPublished is true.

The database will return whatever data the server requests.

Similarly, for creating, editing, and deleting blogs, we can have different end-points to handle the authorization logic in the backend code.

Postgraphile Implementation

The postgraphile implementation can be like below.

postgraphile implementation

The client requests the blogs using a GraphQL query. It also attaches the access token received from the authentication service.

1const data = requestQuery({
2 query: "allBlogs {
3         nodes {
4            content
5            creatorEmail
6            visiblityType
7           }
8        }"
9 accessToken: '***'
10})

In the server, we configure Postgraphile to pass the user information to the database.

1export postgraphile(DATABASE_URL, schemaName, {
2  pgSettings: (req) => {
3     const userEmail = findEmail(accessToken);
4     return({
5         'current_user_email': userEmail
6     })
7  }
8})

We can pass a function as Postgraphile’s pg Settings property, whose return value will be accessible from the connected Postgres database by calling the current_setting function.

In the database, the row-level security policies can be defined to control the data access.

Row-level security policies are basically just SQL that either evaluates to true or false. If a policy is created and enabled for a table, that policy will be checked before doing an operation on the table.

1create policy blogs_policy_select
2on public.blogs for select to users
3USING (
4 isPublished OR
5 creator_email = current_setting('current_user_email')
6);
7ALTER TABLE blogs ENABLE ROW LEVEL SECURITY;

Here the policy named blogs_policy_select will be checked before selecting a row in the table public.blogs. A row will be selected only if the isPublished field is true or creator_email matches with the current user's email.

Similarly, for creating, editing, and deleting blogs, we can have row level security policies for INSERT, UPDATE, and DELETE operations on the table.

Conclusion

The REST implementation does the authorization on the server level but the Postgraphile does it on the database level. Each implementation has its own advantages and disadvantages, which is a topic for another day.

Amal Jose in PostGraphile
January 20, 2021
Share

Subscribe to our newsletter