- 1. Introduce Ruby on Rails Project 1. Introduce Ruby on Rails Project
- 1-2. Set default Ruby version 1-2. Set default Ruby version
- 1-3. Add useful libraries 1-3. Add useful libraries
- 1-4. Setup basic configuration of Ruby on Rails 1-4. Setup basic configuration of Ruby on Rails
- 1-7. Add Bullet Configuration 1-7. Add Bullet Configuration
- 1-8. Modify DB Configuration 1-8. Modify DB Configuration
- 1-9. Modify Puma Configuration 1-9. Modify Puma Configuration
- 1-10. Add a locale file 1-10. Add a locale file
- 1-11. Create dot-env file 1-11. Create dot-env file
- 2. Add some configuration for useful libraries 2. Add some configuration for useful libraries
- 2-1. Prepare initial configuration for annotate 2-1. Prepare initial configuration for annotate
- 2-2. Add RSpec confugration 2-2. Add RSpec confugration
- 2-3. Modify Rubocop configuration 2-3. Modify Rubocop configuration
- 2-4. Initial graphql setup 2-4. Initial graphql setup
- 2-5. Sample GraphQL Resource 2-5. Sample GraphQL Resource
- Conclution Conclution
Here are the steps for building a front-end environment based on Next.js.
We have chosen popular libraries as of September 2020 with the goal of being able to prototype efficiently.
We will use the following as our primary libraries
- Framework: Ruby on Rails
- GraphQL Lib: graphql-ruby, graphql-batch
- Test Framework: RSpec
- Fixure Management: Factorybot
- Linter: Rubocop
- Env Variable Management: dotenv
- Model Annotation: annotate
1. Introduce Ruby on Rails Project
Create a new project:
rails new --api --database=postgresql PROJECT_NAME
1-2. Set default Ruby version
Set default Ruby version as follows:
echo '2.7.1' > .ruby-version
1-3. Add useful libraries
Add useful RubyGem libraries as follows:
# Configuration using ENV
gem 'dotenv-rails'
# GraphQL
gem 'graphql'
gem 'graphql-batch'
# Annotate schema and routes info
gem 'annotate'
# Use Puma as the app server
gem 'puma'
gem 'puma_worker_killer'
group :development, :test do
# help to kill N+1
gem 'bullet'
# Pry & extensions
gem 'pry-byebug'
gem 'pry-rails'
# Show SQL result in Pry console
gem 'awesome_print'
gem 'hirb'
end
group :development do
# A Ruby static code analyzer
gem 'rubocop', require: false
gem 'rails-flog', require: 'flog'
end
group :test do
# Mock for HTTP requests
gem 'vcr'
gem 'webmock'
# test fixture
gem 'factory_bot_rails'
# Time Mock
gem 'timecop'
# Cleaning test data
gem 'database_rewinder'
# Rspec
gem 'rspec-rails'
# This gem brings back assigns to your controller tests
gem 'rails-controller-testing'
end
After that, install thier RubyGem:
bundle install --path vendor/bundle --jobs=4 --without production
1-4. Setup basic configuration of Ruby on Rails
Setup basic configuration of Ruby on Rails with config/application.rb
:
module AppName
class Application < Rails::Application
# Set timezone
config.time_zone = 'Tokyo'
config.active_record.default_timezone = :local
# Set locale
I18n.enforce_available_locales = true
config.i18n.load_path += Dir[Rails.root.join('config', 'locales', '', '*.{rb,yml}').to_s]
config.i18n.default_locale = :ja
# Set generator
config.generators do |g|
g.orm :active_record
g.template_engine false
g.test_framework :rspec, fixture: true
g.fixture_replacement :factory_bot, dir: 'spec/factories'
g.view_specs false
g.controller_specs false
g.routing_specs false
g.helper_specs false
g.request_specs false
g.assets false
g.helper false
end
end
end
1-7. Add Bullet Configuration
Add Bullet Configuration to config/environments/development.rb
as follow:
Rails.application.configure do
# Bullet Setting (help to kill N + 1 query)
config.after_initialize do
Bullet.enable = true # enable Bullet gem, otherwise do nothing
Bullet.alert = true # pop up a JavaScript alert in the browser
Bullet.console = true # log warnings to your browser's console.log
Bullet.rails_logger = true # add warnings directly to the Rails log
end
end
1-8. Modify DB Configuration
Modify Database Configuration config/database.yml
as follows:
default: &default
adapter: postgresql
encoding: unicode
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 50 } %>
development:
<<: *default
database: kikushiru_development
host: 'localhost'
port: <%= ENV['POSTGRES_PORT'] || '5432' %>
username: <%= ENV['POSTGRES_USER'] %>
password: <%= ENV['POSTGRES_PASSWORD'] %>
test:
<<: *default
database: kikushiru_test
host: <%= ENV['POSTGRES_HOST'] || 'localhost' %>
username: <%= ENV['POSTGRES_USER'] %>
password: <%= ENV['POSTGRES_PASSWORD'] %>
production:
<<: *default
database: kikushiru_production
username: kikushiru
password: <%= ENV['POSTGRES_PASSWORD'] %>
Create database.
bundle exec rake db:create
1-9. Modify Puma Configuration
Modify Puma Configuration config/puma.rb
to optimize local development as follows:
# Workers are forked web-server processes
workers ENV.fetch('RAILS_WORKERS') { Rails.env.development? ? 0 : 2 }.to_i
# Puma can serve each request in a thread from an internal thread pool.
# The `threads` method setting takes two numbers a minimum and maximum.
threads_count = ENV.fetch('RAILS_MAX_THREADS') { Rails.env.development? ? 1 : 5 }.to_i
threads threads_count, threads_count
# Specifies the `port` that Puma will listen on to receive requests, default is 3000.
port ENV.fetch('PORT', 3000)
# Specifies the `environment` that Puma will run in.
environment ENV.fetch('RAILS_ENV') { 'development' }
# Use the `preload_app!` method when specifying a `workers` number.
preload_app!
before_fork do
ActiveRecord::Base.connection_pool.disconnect! if defined?(ActiveRecord)
end
on_worker_boot do
# Worker specific setup for Rails 4.1+
# See: https://devcenter.heroku.com/articles/deploying-rails-applications-with-the-puma-web-server#on-worker-boot
ActiveRecord::Base.establish_connection if defined?(ActiveRecord)
end
# Allow puma to be restarted by `rails restart` command.
plugin :tmp_restart
1-10. Add a locale file
Add a local file config/locales
for your user language as follows:
wget https://raw.githubusercontent.com/svenfuchs/rails-i18n/master/rails/locale/ja.yml -P config/locales/ja.yml
1-11. Create dot-env file
Create a dot-env file for development as follows:
touch .env
touch .env.sample
2. Add some configuration for useful libraries
2-1. Prepare initial configuration for annotate
Prepare initial configuration of annotate as follows:
bundle exec rails g annotate:install
2-2. Add RSpec confugration
Prepare initial configuration of RSpec command as follows:
bundle exec rails g rspec:install
echo '--require spec_helper --color -f d' > .rspec
Add RSpec configuration as follows:
require 'factory_bot_rails'
RSpec.configure do |config|
config.order = 'random'
config.before :suite do
DatabaseRewinder.clean_all
end
config.after :each do
DatabaseRewinder.clean
end
config.before :all do
FactoryBot.reload
FactoryBot.factories.clear
FactoryBot.sequences.clear
FactoryBot.find_definitions
end
config.include FactoryBot::Syntax::Methods
VCR.configure do |c|
c.cassette_library_dir = 'spec/vcr'
c.hook_into :webmock
c.allow_http_connections_when_no_cassette = true
end
%i[controller view request].each do |type|
config.include ::Rails::Controller::Testing::TestProcess, type: type
config.include ::Rails::Controller::Testing::TemplateAssertions, type: type
config.include ::Rails::Controller::Testing::Integration, type: type
end
config.define_derived_metadata do |meta|
meta[:aggregate_failures] = true unless meta.key?(:aggregate_failures)
end
end
2-3. Modify Rubocop configuration
Modify code by Rubocop and generate configuration as following commands:
bundle exec rubocop --auto-correct
bundle exec rubocop --auto-gen-config
2-4. Initial graphql setup
Setup initial graphql configuration as following commands:
rails g graphql:install
Create app/graphql/foreign_key_loader.rb
for graphql-batch:
class ForeignKeyLoader < GraphQL::Batch::Loader
attr_reader :model, :foreign_key, :scopes
def self.loader_key_for(*group_args)
# avoiding including the `scopes` lambda in loader key
# each lambda is unique which defeats the purpose of
# grouping queries together
[self].concat(group_args.slice(0, 2))
end
def initialize(model, foreign_key, scopes: nil)
super()
@model = model
@foreign_key = foreign_key
@scopes = scopes
end
def perform(foreign_ids)
# find all the records
filtered = model.where(foreign_key => foreign_ids)
filtered = filtered.merge(scopes) if scopes.present?
records = filtered.to_a
foreign_ids.each do |foreign_id|
# find the records required to fulfill each promise
matching_records = records.select do |r|
foreign_id == r.send(foreign_key)
end
fulfill(foreign_id, matching_records)
end
end
end
Create app/graphql/record_loader.rb
for graphql-batch:
class RecordLoader < GraphQL::Batch::Loader
def initialize(model, column: model.primary_key, where: nil)
super()
@model = model
@column = column.to_s
@column_type = model.type_for_attribute(@column)
@where = where
end
def load(key)
super(@column_type.cast(key))
end
def perform(keys)
query(keys).each { |record| fulfill(record.public_send(@column), record) }
keys.each { |key| fulfill(key, nil) unless fulfilled?(key) }
end
private
def query(keys)
scope = @model
scope = scope.where(@where) if @where
scope.where(@column => keys)
end
end
Modify GraphQL schema information of app/graphql/app_name_schema.rb
to use graphql-batch as follows:
class AppNameSchema < GraphQL::Schema
mutation(Types::MutationType)
query(Types::QueryType)
# Opt in to the new runtime (default in future graphql-ruby versions)
use GraphQL::Execution::Interpreter
use GraphQL::Analysis::AST
use GraphQL::Batch
# Add built-in connections for pagination
use GraphQL::Pagination::Connections
end
2-5. Sample GraphQL Resource
rails g model article title:string body:text
Create a sample GraphQL resolver with app/graphql/resolvers/article.rb
as follows:
module Resolvers
class Article < Base
description 'Fetch article'
type Types::ArtcileType, null: false
argument :id, Int, required: true, description: 'ID'
def resolve(id:)
RecordLoader.new(::Article).load(id)
end
end
end
Create a sample GraphQL type with app/graphql/types/article_type.rb
as follows:
module Types
class ArticleType < Types::BaseObject
field :id, Int, null: false
field :title, String
field :body, String
field :created_at, GraphQL::Types::ISO8601DateTime, null: false
field :updated_at, GraphQL::Types::ISO8601DateTime, null: false
end
end
Add the Types::ArticleType
to QueryType
as follows:
module Types
class QueryType < Types::BaseObject
field :article, type: Types::ArticleType, null: true, resolver: Resolvers::Article
end
end
Conclution
Happy Hacking!!