20200804のRailsに関する記事は16件です。

gem sitemap_generator で priority, lastmod, changefreq を記載しない方法

概要

sitemap_generator ではデフォルトでは priority, lastmod, changefreq にデフォルト値が記載されます。
これらの項目を出力したくないときもあるのでその方法を共有します。

方法

priority, lastmod, changefreq に nil を渡すと出力されなくなります。

add '/home', priority: nil, lastmod: nil, changefreq: nil
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Ruby] 親モジュールを取得する

概要

以下のようなモジュールの階層があるときに、ChildモジュールからParentモジュールのVALUEを参照したかった。

module Parent
  VALUE = 'PARENT'

  module Child
    VALUE = 'CHILD'

    def self.parent_value
      # 'PARENT' がほしい
    end
  end
end

Rails(activesupport) が使える環境なら簡単

流石に Module#parent 的なメソッドがRubyで提供されてるだろうと思い調べて見たが、どうやらRuby(2.7現在)本体にはそのような機能はなく、Rails の拡張(activesupport)で提供されていることがわかった。

Module#parent

そのため、Rails環境では以下のように単純にparentを参照することで対応可能

module Parent
  VALUE = 'PARENT'

  module Child
    VALUE = 'CHILD'

    def self.parent_value
      self.parent::VALUE
    end
  end
end
[4] pry(main)> Parent::Child.parent_value
=> "PARENT"

Rails以外でも使いたいので実装する

Railsでない(activesupportが使用できない) 状態の場合、以下のようにparentメソッドがないよと怒られてしまう。

irb(main):008:0> Parent::Child.parent_value
Traceback (most recent call last):
        5: from /Users/shingo.sasaki/.rbenv/versions/2.6.5/bin/irb:23:in `<main>'
        4: from /Users/shingo.sasaki/.rbenv/versions/2.6.5/bin/irb:23:in `load'
        3: from /Users/shingo.sasaki/.rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/irb-1.0.0/exe/irb:11:in `<top (required)>'
        2: from (irb):8
        1: from /Users/shingo.sasaki/Docker/teachme/app/libraries/hoge.rb:8:in `parent_value'
NoMethodError (undefined method `parent' for Parent::Child:Module)

activesupport では以下のように実装されている。

def parent
  parent_name ? ActiveSupport::Inflector.constantize(parent_name) : Object
end
def parent_name
  if defined?(@parent_name)
    @parent_name
  else
    parent_name = name =~ /::[^:]+\Z/ ? $`.freeze : nil
    @parent_name = parent_name unless frozen?
    parent_name
  end
end

と、コードを追っていくと以下のような手段を用いていることがわかる

  • Module#name を使って、モジュール名を文字列で取得
  • :: を元に、親モジュールまでのモジュール名を正規表現で抽出
  • Object.const_get を用いて文字列からモジュールを取得

よって、Moduleクラス自体を以下のように拡張すれば近いことができる(シンプルさを重視して、実際のactivesupportほど手広くカバーしてません)

class Module
  def parent
    parent_name = self.name =~ /::[^:]+\Z/ ? $`.freeze : nil
    parent_name ? Object.const_get(parent_name) : Object
  end
end

↑を読み込んだ状態ならこんな構造があっても

module Parent
  module Child
    module GrandChild
    end
  end
end

階層をたどることが出来る

[3] pry(main)> Parent::Child::GrandChild.parent
=> Parent::Child
[4] pry(main)> Parent::Child::GrandChild.parent.parent
=> Parent
[5] pry(main)> Parent::Child::GrandChild.parent.parent.parent
=> Object
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Railsでランキング機能を実装する方法

以前、Qiitaのこの記事(Railsでお手軽ランキング機能)を参考にして、ランキング機能を実装したことがありました。
無事、実装できたのですが、以下の問題に直面しました。

kaminariのページネーション機能と組み合わせると上手くいかない!

@posts = Post.find(Like.group(:post_id).order('count(post_id) desc').pluck(:post_id)).page(params[:page])

kaminariのpageメソッドと組み合わせると上記のようになりますが、これだと以下のようなエラーが発生します。
スクリーンショット 2020-08-04 16.58.47.png

そのためpageメソッドと組み合わせても、問題ないランキング機能をご紹介いたします。

前提

  • 記事のテーブル名は posts
  • いいねのテーブル名は likes
  • すでにいいね機能を実装しており、postsとlikesのアソシエーションができていること

ランキング機能の実装

@posts = Post.joins(:likes).group(:post_id).order('count(post_id) desc')

これでOKです!
一つずつ説明します。

Post.joins(:likes)  #postsテーブルとlikesテーブルを内部結合します
group(:post_id)  #post_idが同じものにグループを分けます
order('count(post_id) desc')  #それをpost_idの多い順番に並び替える

これでランキング機能は完成です。
以下のように、コードの末尾にpageメソッドをつけてもエラーにならず、ページネーション機能が機能していると思います。

@posts = Post.joins(:likes).group(:post_id).order('count(post_id) desc').page(params[:page])

参考文献

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

本番環境とcredentials.yml.enc

この記事で伝えたいこと

production環境の秘匿情報をcredentialsで扱う前にしっかり調べましょう
ただのコピペダメ絶対。

基礎知識 暗号化と復号について

〜開発環境〜
$ rails newした時config/master.keyと共にcredentials.yml.encは作成されるようです。
そしてこのmaster.keyを使用して暗号化、復号します。
(master.keyは大切に保管しましょう)

秘匿情報の編集には以下のコマンドを実行します。

$ rails credentials:edit          #master.keyが存在しない時実行すると新たに作成する

〜本番環境〜
暗号化と復号にはsecret_key_baseも必要になります。
ローカルで$ rails secretコマンドを実行し作成します。
事前にローカルのmaster.keyをサーバーにも配置しておくこと。

本番環境で気をつけること

credentials.yml.encの暗号化、復号にはmaster.keyを使用すると先ほど書きました。このmaster.key、デフォルトでgitignoreに登録されているためGitの管理対象外となっています。

ここからが大切です。
EC2でgitのリポジトリをクローンしてもこのmaster.keyは当然サーバー上にやってきません。
その事を忘れて本番環境の秘匿情報を追加しようと思い$ rails credentials:editコマンドを実行すると...サーバー上にはmaster.keyが無いので新たに生成されてしまいます。

この時点ではローカルのmaster.keyとサーバーのmaster.keyが異なりcredentials.yml.encの復号ができなくなります。あら大変。

Couldn't decrypt config/credentials.yml.enc. Perhaps you passed the wrong key?

こんなエラーや、

ActiveSupport::MessageEncryptor::InvalidMessage

こんなエラーが発生します。

もう一度credentialsの復号がしたい

ローカルのmaster.keyをサーバーに置いてあげれば良いです。

master.keyを紛失した場合は、config/credentials.yml.encを削除してから以下のコマンドで
新たなものを生成してくれるようです。
ただしcredentialsの中身は全て吹き飛びますのでご注意を。

$ sudo EDITOR=vim rails credentials:edit

Rails6以降とcredentilas.yml.enc

6以降は環境ごとに秘匿情報を分けられるようになりました。(祝)

本番環境で情報を追加したい時以下コマンドを実行します。
環境に応じてenvironment以降を変化させます。

$ rails credentials:edit --environment production

このコマンドはconfig/credentials/production.yml.encと、config/credentials/production.keyを作成します。ファイル名とキーの名前にそれぞれ該当する環境が記載されます。
サーバーにはproduction.keyのみを上げれば良い。

この場合でもmaster.keyやsecret_key_baseの扱いには注意。

参考

Rails 5.2 で ActiveSupport::MessageEncryptor::InvalidMessage

Rails5.2から追加された credentials.yml.enc のキホン

Rails5.2の新機能credentials等でパスワード等を管理する

【Ruby/Rails】デプロイ作業をCapistranoで自動化する

Add support for multi environment credentials.

Rails6から入ったmulti environment credentialsを使う

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Flexboxで子要素の幅を指定する

プログラミングの勉強日記

2020年8月4日 Progate Lv.226
Flexboxでwidthが効かなかったので原因を調べて解決した。(メモ)
flex-shrinkを0にすることでwidthの指定をすることもできるみたいだが、今回はwidthを使うのをやめてflex-basicを使った。

直面した問題

 Flexboxで指定すると、画面幅によって子要素が伸びてしまい、綺麗に見れなくなってしまった。
0804.png
 下の画像のように子要素の幅を指定することで、画面幅によって子要素が伸びないようにしたい。
0804-1.png

解決方法

 flex-basisを使う。子要素に対してwidthと同じように%やpxで幅の値を指定することができる。初期値はautoになっていて、autoと指定した場合は子要素のコンテンツのサイズが適応される。

html
<div class="parent">
  <div class="children">
    <!--省略-->
  </div>
</div>
css
.parent{
  display:flex;
}

.children{
  flex-basis: 500px;
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

rails routes.rbのmemberとcollectionの違い

routes.rbのmemberとcollectionの違い

railsのroutes.rbでルーティングを設定する時にmemberとcollectionの違いに関して記載します。

menberの場合

routes.rb
resources :buy_additional_actions, only: %i[] do
   member do
      get 'index' => 'buy_additional_actions#index'

menberの場合は生成されたurlに:idが自動で追加されます。

buy_additional_action GET /buy_additional_actions/:id/index(.:format) buy_additional_actions#index

collectionの場合

routes.rb
resources :buy_additional_actions, only: %i[] do
   collection do
      get 'index' => 'buy_additional_actions#index'

collectionの場合はurlには:idが付与されません。

buy_additional_actions GET /buy_additional_actions/index(.:format) buy_additional_actions#index
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

rails5中級チュートリアル中に発生したSassC::SyntaxError in Pages#index対処法

rails5中級チュートリアル中にエラーが発生 以下エラー画像
415e5c6c8622f2ea3ffb780959431d92.png

仮説と試したこと

エラー文の解説を翻訳してみると

「エラー:インポートするファイルが見つからないか、読み取れません:bootstrap-sprockets。
app / assets / stylesheets / application.scssの15:1行
@import "bootstrap-sprockets";」

となる。 importの読み込み記述はされているが、sprocketsが存在しないことになっていると仮説。

エラー文で検索した際に出てくる多くのサイトではrails sをしていなかったのが原因と述べられているものが多いが、自分の場合rails sでの再起動を行うが効果なし。
参考サイト

解決

application.scssをいじっていたら解決する事ができた

application.scss修正前
@import "bootstrap-sprockets";
@import "bootstrap";
@import "protospace";

三行目を削除

application.scss修正後
@import "bootstrap-sprockets";
@import "bootstrap";

どうやら解決するためにいくつかサイトを見ていた際、誤ってprotospaceをimport読み込みしていたのが原因だったらしい。
そのため解決法は rails s不足と誤ったimportを記述してしまっていた事であった。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ソースコードを読むとき

既存プロジェクトに入った際、各Classがどのようなメソッドをもっているかを確認する必要があると思います。

その際、自分がどのように確認を行っているかを以下で述べていこうと思います。

[環境]
Ruby
Rails
Solidus

今回は、RailsのSolidusというGemを使って、説明させて頂ければと思います。
download.png

$rails c
でコンソールの中に入る。

そこで、上記のように、任意の文字にオブジェクトを代入する。
(Spree::Taxonはオブジェクト)

$a.methods
を実行。実行することで、そのオブジェクトが持っているmethodが表示される。

download (1).png

$a.methods.grep /product/
とすると、productにまつわるmethodのみを取得できる。
上記のようにgrepを使う事で検索結果を絞って検索する事ができるので、結構使う機会も多いと思います。

download (2).png

$a.method(:before_remove_for_products=).source_location
上記のように、method(メソッド名).source_locationとすることで、
どこのファイルにそのメソッドがあるかを探す事もできます。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

既存のRailsアプリの開発環境にDockerを導入する手順【Rails, MySQL, Docker】

こんにちは.
今回は, 既存のRailsアプリの開発環境にDockerを導入する手順をまとめてみました.
まだまだ勉強不足ですので, 修正点・改善点等ございましたら, ご指摘いただけますと幸いです.

事前準備

環境

Ruby:2.5.3
Rails:5.2.4.3
MySQL:5.6
Docker:19.03.8
docker-compose:1.25.4

手順

1. ルートディレクトリにDockerfile,docker-compose.ymlを追加

add_file.png

既存のRailsアプリのルートディレクトリ直下にDockerfiledocker-compose.ymlを作成します.
以下, それぞれのファイルの中身です.

Dockerfile

Dockerfile
FROM ruby:2.5.3
RUN apt-get update && apt-get install -y \
    build-essential \
    nodejs
WORKDIR /kakeibo
COPY Gemfile Gemfile.lock /kakeibo/
RUN bundle install
  • FROM ruby:2.5.3の部分についてはアプリのRubyのバージョンに合わせる.
  • RUN apt-get update && apt-get install -y ~で必要なパッケージをインストールする.
  • WORKDIR /kakeiboでコンテナ内にフォルダを作成.
  • COPY Gemfile Gemfile.lock /kakeibo/でコンテナ内にGemfileとGemfile.lockをコピーした後, bundle installを実行する.

docker-compose.yml

docker-compose.yml
version: '3'

volumes:
  mysql-data:

services:
  web:
    build: .
    command: bundle exec rails s -p 3000 -b '0.0.0.0'
    ports:
      - '3000:3000'
    volumes:
      - '.:/kakeibo'
    tty: true
    stdin_open: true
    depends_on:
      - db
    links:
      - db

  db:
    image: mysql:5.6
    volumes:
      - 'mysql-data:/var/lib/mysql'
    environment:
      - 'MYSQL_ROOT_PASSWORD=password'

Dockerfiledocker-compose.ymlの中身の詳しい説明はこちらの記事にわかりやすくまとめられていました.

2. config/database.ymlを編集

config/database.yml
default: &default
  adapter: mysql2
  encoding: utf8
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  username: root
  password: password
  host: db

development:
  <<: *default
  database: kakeibo_development

config/database.ymlpasswordhostをdocker-compose.ymlで設定した値に合わせます.

3. コンテナ起動

terminal
$ docker-compose build
$ docker-compose up -d
$ docker-compose exec web rails db:create
$ docker-compose exec web rails db:migrate

これで http://localhost:3000 にアクセスすると無事にアプリが表示されるはずです.

参考

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Railsアプリの開発環境をDockerで構築する手順【Rails, MySQL, Docker】

こんにちは.
今回は, Railsアプリの開発環境をDockerで構築する手順をまとめてみました.
まだまだ勉強不足ですので, 修正点・改善点等ございましたら, ご指摘いただけますと幸いです.

事前準備

環境

Ruby: 2.5.8
Rails: 5.2.4.3
MySQL: 5.7.31
Docker: 19.03.8
Docker Compose: 1.25.4

手順

1. プロジェクトのルートディレクトリを作成

terminal
$ mkdir test-app

初めに, プロジェクトのルートディレクトリを作成します.

2. ルートディレクトリ直下にファイルを追加

terminal
$ cd test-app
$ touch Dockerfile docker-compose.yml Gemfile Gemfile.lock

作成したルートディレクトリの直下にDockerfile, docker-compose.yml, Gemfile, Gemfile.lockの4つのファイルを作成します.
それぞれのファイルの中身は以下のようになります. (Gemfile.lockは空のままにします.)

Dockerfile
FROM ruby:2.5
RUN apt-get update && apt-get install -y \
    build-essential \
    nodejs
WORKDIR /test-app
COPY Gemfile Gemfile.lock /test-app/
RUN bundle install
docker-compose.yml
version: '3'

volumes:
  mysql-data:

services:
  web:
    build: .
    command: bundle exec rails s -p 3000 -b '0.0.0.0'
    ports:
      - '3000:3000'
    volumes:
      - '.:/test-app'
    tty: true
    stdin_open: true
    depends_on:
      - db
    links:
      - db

  db:
    image: mysql:5.7
    volumes:
      - 'mysql-data:/var/lib/mysql'
    environment:
      - 'MYSQL_ROOT_PASSWORD=test-app'
Gemfile
source 'https://rubygems.org'
gem 'rails', '~>5.2'

3. コンテナ内にRailsのセットアップを行う

terminal
$ docker-compose run --rm web rails new . --force --database=mysql --skip-bundle --skip-test

webのコンテナ内でrails newを実行します.
今回はテストにRSpecを使用する予定でしたので, --skip-testも追加しています.

4. 作成されたconfig/database.ymlを編集

Railsのセットアップにより作成されたconfig/database.ymlを以下のように編集します.

config/database.yml
default: &default
  adapter: mysql2
  encoding: utf8
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  username: root
  password: test-app   #docker-compose.ymlのMYSQL_ROOT_PASSWORDの値を設定する
  host: db   #docker-compose.ymlのservice名と合わせる

development:
  <<: *default
  database: test-app_development

5. コンテナの起動

terminal
$ docker-compose up --build -d
$ docker-compose run --rm web rails db:create

これで, http://localhost:3000 にアクセスすると, Railsのホーム画面が表示されるはずです.

rails_home_pic.png

参考

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

graphql-rubyを使って認可する方法

GraphQLを使っているときに様々な処理で認可させたい事があると思います。

  • このQueryはログインユーザーのみ実行できるようにしたい
  • このMutationは管理者のみ実行できるようにしたい
  • このQueryは自分の所有しているデータのときだけ返却するようにしたい
  • このFieldは自分の所有しているデータのときだけ返却するようにしたい

当初はgraphql-rubyの知識が乏しかったので取得や更新処理の中で認可する処理を呼び出していたのですが、graphql-rubyのドキュメントを改めて読み直したところ、認可のためのメソッド(authorized?)がある事がわかったので動作検証を兼ねて記事を書きました。

graphql-rubyについて

Ruby(Rails)でGraphQLを簡単に使えるようにしてくれるGemです。
https://github.com/rmosolgo/graphql-ruby

細かいところは実際に試してみないとわからないことも多いですが、ドキュメントが充実していて素晴らしいです。
https://graphql-ruby.org/guides

この記事を書いている時点では、graphql: 1.11.1を使っています。
まだガンガンバージョンアップしているGemなので、バージョンが違うと大幅に動作が変わっている可能性があるのでご注意ください。

認可の実装例

最初に挙げた4つのパターンの実装例を説明します。

前提条件

認可に必要なログインユーザーの情報はcontextに格納していることとします。
認証についてはこの記事の本筋からの逸れるので説明は省略します。

app/controllers/graphql_controller.rb
# ログインユーザーの情報はcontext[:current_user]に格納
# 未ログインの場合はnil
context = { current_user: current_user }

このQueryはログインユーザーのみ実行できるようにしたい

ここでは『review_idを指定して該当するReviewTypeを返却するクエリー』を実装します。

認可を入れる前

認可を実装する前にReviewTypeを取得するクエリーを実装します。

app/graphql/types/query_type.rb
module Types
  class QueryType < Types::BaseObject
    field :review, resolver: Resolvers::ReviewResolver
  end
end
app/graphql/resolvers/review_resolver.rb
module Resolvers
  class ReviewResolver < BaseResolver
    type Types::ReviewType, null: true

    argument :review_id, Int, required: true

    def resolve(review_id:)
      Review.find_by(id: review_id)
    end
  end
end
app/graphql/types/review_type.rb
module Types
  class ReviewType < BaseObject
    field :id, ID, null: false
    field :title, String, null: true
    field :body, String, null: true
    field :secret, String, null: true
    field :user, Types::UserType, null: false
  end
end
app/graphql/types/user_type.rb
module Types
  class UserType < BaseObject
    field :id, ID, null: false
    field :name, String, null: false
    field :email, String, null: false
  end
end

GraphiQLで実行すると次のようになります。
スクリーンショット 2020-07-24 13.51.33.png

認可を実装

それでは先ほど実装した処理に『ログインユーザーのみ実行できる』という制約を追加します。

authorized?を使わない実装

以前の私はresolveメソッドでReviewを取得する前にログインチェックする実装を入れていました。

まずは様々なResolverから使えるようにBaseResolverにログインチェックメソッドを実装します。
context[:current_user]が入っていない場合はエラーを発生させます。
ちなみに、GraphQL::ExecutionErrorを使うとraiseするだけでレスポンスをGraphQLのエラー形式に変換してくれます。

app/graphql/resolvers/base_resolver.rb
 module Resolvers
   class BaseResolver < GraphQL::Schema::Resolver
     def login_required!
       # ログインしていなかったらraise
       raise GraphQL::ExecutionError, 'login required!!' unless context[:current_user]
     end
   end
 end

次にBaseResolverのログインチェックを処理の最初に呼び出すようにします。

app/graphql/resolvers/review_resolver.rb
 def resolve(review_id:)
+  # 処理の最初にログインチェックを行う
+  login_required!

   Review.find_by(id: review_id)
 end

GraphiQLで未ログインの状態で実行すると次のようになります。
スクリーンショット 2020-07-25 12.53.22.png

この方法でもやりたいことは実現できているのですが、ログイン必須のResolverは処理の最初に必ずlogin_required!を書かなければいけません。
controllerのbefore_actionのように本処理が呼ばれる前に自動で認可してくれる方法はないのかをずっと探していました。

authorized?を使う実装

graphql-rubyのガイドを改めて読んでいるとauthorized?というメソッドがあることに気づきました。
これを使うとresolveメソッドの前に認可を行い、実行可否を制御することができるようです。
下記はmutationに追加するガイドですが、Resolverにも同じように追加できます。
https://graphql-ruby.org/mutations/mutation_authorization.html

ログイン必須のResolverは汎用的に使えそうなので、ログイン必須のResolverが継承するlogin_required_resolverを作りました。
authorized?のパラメーター(args)にはresolveと同じパラメーターが格納されます。

app/graphql/resolvers/login_required_resolver.rb
module Resolvers
  class LoginRequiredResolver < BaseResolver
    def authorized?(args)
      context[:current_user].present?
    end
  end
end

review_resolverはlogin_required_resolverを継承するように修正します。
他の実装は認可を追加する前と同じです。

app/graphql/resolvers/review_resolver.rb
- class ReviewResolver < BaseResolver
+ class ReviewResolver < LoginRequiredResolver

GraphiQLで未ログインの状態で実行すると次のようになります。
スクリーンショット 2020-07-25 13.01.45.png

authorized?の結果がfalseの場合はエラー情報はなくdata: nullだけ返却されるようになりました。
ガイドにも記載がある通り、authorized?がfalseの場合はdata: nullだけを返却するのがデフォルトの挙動のようです。
nullを返却するという仕様で問題なければこのままで良いですが、認可されない場合はエラー情報も返却するように変更してみます。

エラー情報を追加する方法は簡単で、authorized?の中でGraphQL::ExecutionErrorをraiseすればできます。
ちなみに成功時はtrueを明示的に返却しないと成功と認識されないので注意が必要です。

app/graphql/resolvers/login_required_resolver.rb
module Resolvers
  class LoginRequiredResolver < BaseResolver
    def authorized?(args)
      # 認可できない場合はGraphQL::ExecutionErrorをraise
      raise GraphQL::ExecutionError, 'login required!!' unless context[:current_user]

      true
    end
  end
end

GraphiQLで未ログインの状態で実行すると次のようになります。
これでauthorized?を使った場合でもエラー情報を返却することができました。
スクリーンショット 2020-07-25 13.25.54.png

authorized?を使った場合、resolveメソッドでは認可の処理を書く必要がなくなるのでシンプルに書くことができます。
(今回の例はかなりシンプルな実装なのでそこまで差はありませんが・・・)

このMutationは管理者のみ実行できるようにしたい

ここでは『review_idを指定して該当するReviewのtitleとbodyを更新するMutation』を実装します。

認可を入れる前に

認可を実装する前にReviewを更新するMutationを実装します。
1つ前の例で使ったReviewTypeなどそのまま使うクラスは省略します。

app/graphql/types/mutation_type.rb
module Types
  class MutationType < Types::BaseObject
    field :update_review, mutation: Mutations::UpdateReview
  end
end
app/graphql/mutations/update_review.rb
module Mutations
  class UpdateReview < BaseMutation
    argument :review_id, Int, required: true
    argument :title, String, required: false
    argument :body, String, required: false

    type Types::ReviewType

    def resolve(review_id:, title: nil, body: nil)
      review = Review.find review_id
      review.title = title if title
      review.body = body if body
      review.save!

      review
    end
  end
end

GraphiQLで実行すると次のようになり、Reviewデータが更新されます。
スクリーンショット 2020-07-27 11.34.17.png

認可を実装

Mutationでも先程の例と同様にauthorized?を使うことができます。
下記のガイトに記載されています。
https://graphql-ruby.org/mutations/mutation_authorization.html

管理者しか利用できないMutationが継承する親クラスを作って継承するようにします。

app/graphql/mutations/base_admin_mutation.rb
module Mutations
  class BaseAdminMutation < BaseMutation
    def authorized?(args)
      raise GraphQL::ExecutionError, 'login required!!' unless context[:current_user]
      raise GraphQL::ExecutionError, 'permission denied!!' unless context[:current_user].admin?

      super
    end
  end
end
app/graphql/mutations/update_review.rb
- class UpdateReview < BaseMutation
+ class UpdateReview < BaseAdminMutation

Mutationのauthorized?もfalseを返却するだけだとエラー情報は返却されず、dataがnullになり更新処理が実行されないようになります。
Resolverはそれでも良さそうですがMutationはエラー情報を返却しないとよくわからないと思うので、こちらもGraphQL::ExecutionErrorをraiseするように実装しました。
ちなみにガイドを読むと下記のように戻り値にerrorsを返却することでエラー情報を返す方法もあるようです。
試してみましたが下記の方法ではerrors配下のlocationsやpathは返却されませんでしたが、errorsのmessageは返却できました。
メッセージだけ返却できればよいのであればどちらの方法で実装しても良さそうです。

def authorized?(employee:)
  if context[:current_user]&.admin?
    true
  else
    return false, { errors: ["permission denied!!"] }
  end
end

GraphiQLで管理者権限を持っていないユーザーが実行すると次のようになります。
もちろんエラーの場合は更新処理は実行されません。
スクリーンショット 2020-07-27 11.48.29.png

このQueryは自分の所有しているデータのときだけ返却するようにしたい

ここでは最初に作った『review_idを指定して該当するReviewTypeを返却するクエリー』を基に改修します。
最初に作ったものはログイン状態のみ確認していましたが、今回はReviewが自分の所有物か?のチェックを追加します。

ログインチェックと同じauthorized?に実装してみる

ログインチェックと同じauthorized?にチェックを追加できればよいのですが、今回のチェックはRevewを取得した後でないとチェックできません。
authorized?でもreview_idは引数で受け取るのでReviewを取得することもできるのですが、そうするとresolveの役割が曖昧になります。
実際に実装してみます。

app/graphql/resolvers/login_required_resolver.rb
 def authorized?(args)
   raise GraphQL::ExecutionError, 'login required!!' if context[:current_user].blank?

+   # この時点でreviewの取得が必要
+   review = Review.find_by(id: args[:review_id])
+   return false unless review
+   raise GraphQL::ExecutionError, 'permission denied!!' if context[:current_user].id != review.user_id

   true
 end

authorized?でReviewの取得が必要になります。
resolveメソッドでも取得するので、ここでも取得すると非効率な気がしますね。
では、resolve側にチェックを実装するとどうでしょうか?

app/graphql/resolvers/review_resolver.rb
 def resolve(review_id:)
-   Review.find_by(id: review_id)
+   review = Review.find_by(id: review_id)
+   raise GraphQL::ExecutionError, 'permission denied!!' if context[:current_user].id != review.user_id

+   review
 end

こちらの方がauthorized?で実装するより効率は良さそうですが、authorized?にチェック処理を切り出すことでデータ取得処理のみ記載していたresolveにまたチェック処理が入ってしまいました。

当初はデータ取得後にしかチェックできないものがresolveでチェックするしかないと思っていたのですが、authorized?はReviewTypeにも定義できることを知ったのでReviewTypeに定義してみます。

ReviewTypeでチェックする

ReviewTypeでチェックするとはどういうことなのか?
実際に実装してみます。

ReviewTypeは誰でも使えるようにしておきたいので、MyReviewTypeという自分しか閲覧できない制約をつけたReviewTypeを作ります。

app/graphql/types/my_review_type.rb
module Types
  class MyReviewType < ReviewType
    def self.authorized?(object, context)
      raise GraphQL::ExecutionError, 'permission denied!!' if context[:current_user].id != object.user_id

      true
    end
  end
end

ガイドにも記載されていますが、Typeで使うauthorized?はobjectとcontextを引数に受け取ります。
あと、クラスメソッドなので注意が必要です。
https://graphql-ruby.org/authorization/authorization.html

あとはレスポンスのTypeをMyReviewTypeにするだけです。他の修正は不要です。

app/graphql/resolvers/review_resolver.rb
- type Types::ReviewType, null: true
+ type Types::MyReviewType, null: true

GraphiQLで自分以外のReviewを指定すると次のようになります。
スクリーンショット 2020-07-27 22.40.15.png

これでresolveメソッドには認可の処理を書く必要がなくなるのでシンプルに書くことができました。
また、レスポンスをMyReviewTypeにすることでスキーマ定義を読むだけで、このクエリーはMyReviewTypeを返却する=「自分しか閲覧できない」ということが明確になるので良いと思います。

このFieldはログインユーザー自身のデータのときだけ返却する

1つ前の例ではMyReviewTypeを定義してレスポンス全体を自分のデータのときしか見れないようにしました。
しかし、全部ではなく特定のフィールドだけ見れないようにしたいこともあると思います。

ReviewTypeを再掲します。
ここではsecretカラムは自分のデータしか見れないようにしたいと思います。

app/graphql/types/review_type.rb
module Types
  class ReviewType < BaseObject
    field :id, ID, null: false
    field :title, String, null: true
    field :body, String, null: true
    field :secret, String, null: true # <- これを自分の場合のみ見えるようにする
    field :user, Types::UserType, null: false
  end
end

ガイドを読むとfieldにもauthorized?が実装できるようなのですが、1つのfieldだけをカスタマイズするのは難しそうなのでここではauthorized?を使わずに実装することにしました。
https://graphql-ruby.org/authorization/authorization.html
fieldのガイドはこちら
https://graphql-ruby.org/fields/introduction.html#field-parameter-default-values

下記のようにfield名と同じメソッドを定義すると、そちらが呼び出されるようになります。
そのメソッド内に認可を実装しました。

app/graphql/types/review_type.rb
module Types
  class ReviewType < BaseObject
    field :id, ID, null: false
    field :title, String, null: true
    field :body, String, null: true
    field :secret, String, null: true
    field :user, Types::UserType, null: false

    # field名のメソッドを定義すると呼び出される
    def secret
      # ログインユーザーとレビューを書いたユーザーが違う場合、nilを返却
      return if object.user_id != context[:current_user].id

      object.secret
    end
  end
end

GraphiQLで自分以外のReviewを指定すると次のようになります。
secretはnullが返却されています。
スクリーンショット 2020-07-28 22.51.28.png

Resolverにこのチェックを実装するとReviewTypeを使うすべてのResolverがsecretの考慮をしなければいけなくなりますが、ReviewTypeに実装することで個別のResolverはsecretのアクセス制御を考える必要がなくなります。

最後に

graphql-rubyを使い始める前にもガイドは一通り目を通したつもりだったのですが、authorized?の存在は見落としていました・・・
authorized?以外にもまだまだ気づいていない便利な機能がありそうですね。
また、今はなかったとしてもがんがんバージョンアップされており、これからも新しい機能が追加される可能性も高いので、これからもgraphql-rubyの動向をチェックしていきたいと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

railsでのboorstrapの導入

自分用です!

bootstrapを導入していきます。

Gemfile
gem 'bootstrap', '~> 4.4.1'
gem 'font-awesome-sass', '~> 5.12.0'
gem 'jquery-rails'

Gemfileに記載をし、bundle installを実行。

ちなみに、公式のgemを検索するときは、rubygemsがおすすめ。

また gem 'font-awesome-rails'では、font-awesome5系に対応していないので gem 'font-awesome-sass'を使用する必要があります。

application.scss
@import 'bootstrap';
@import 'font-awesome-sprockets';
@import 'font-awesome';

を追加。

application.js
//= require jquery3
//= require popper
//= require bootstrap-sprockets

を追加してください。

//= require_tree . は一番下の行に記載していること
※ ここではrequireするJavaScriptファイルの順番に注意してください。
//= require jquery3よりも前に//= require_tree .を記載した場合、 //= require_tree .で読み込んだファイルでjqueryのコードを参照しているとjquery3が読み込まれる前に評価されるためメソッドが未定義となりエラーになるケースが後の課題で発生します。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

scssの入れ子とパーシャル

自分用です!

sassのimport

railsでは、cssをsassで書いていきます。

・scssにすることで変数や親子関係の定義など、効率化の機能が使用でき、全体のコード量を削減できます。
・sass記法ではインデント制御のためデザイナーに引き渡しづらく、cssの記法から離れてしまうという面からscss記法を採用しています。

application.scssとtop.scssがあるとして、top.scssをapplication.scssにインポートします。

application.scss
@import 'top';

このように、@importを使えば必要なcssファイルだけをインポートできます。

パーシャル

次に、sassと同じようにviewでも入れ子構造が使えます。
それがパーシャルです。

application.html.erbにheaderとfooterのコードがたくさん入っていていると見えずらいですよね。

そこで、shared/_header.html.erbと、shared/_footer.html.erbを作成し、そこにヘッダーとフッターのコードをかき、application.htmlに入れ子する。

application.html.erb
<body>
  <%= render 'shared/header' %>
  <%= yield %>
  <%= render 'shared/footer' %>
</body>

このようにすれば、見やすくなる。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

rails gの際のファイル生成の設定

自分用です!

rails g コマンドを使用した際に、assets、helper、testファイル、ルーティングが生成されないように設定して行きます。

config/application.rb
 config.generators do |g| 

      g.skip_routes true 
      g.assets false 
      g.helper false 
      g.test_framework false 
   end

これで、無駄なファイルが生成されない。

ちなみに、、、

環境設定をする際は、

config/initializers以下の各ファイル
特定のツールや機能に対する設定ファイルを、ファイル別に記述する。(assets.rb / aws.rbなど)

config/environments以下
環境別に行いたい初期化作業を記述する
(production.rb / staging.rb など)

config/applicaiton.rb
アプリケーション全体に関する設定を行う

順序に寄らないものに関しては、基本的に初期化作業内で全て実行されますが、後々のメンテナンスのしやすさ等を考慮し、上記のようなディレクトリ構成でファイルを分けていくことが無難。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

モデル、コントローラーの生成

自分用のメモです!

コントローラーの生成

ex) (newアクションを持つ)userコントローラーの生成

ターミナル
rails g controller Users new

モデルの作成

ex) (name,emailカラムをもつ)userモデルの生成

ターミナル
rails g model User name:string email:string

コントローラーの削除

ex) userコントローラーの削除

ターミナル
rails destroy controller Users

モデルの削除

 
ex) userモデルの削除

ターミナル
rails destroy model User
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[rails]部分テンプレートの作成方法

部分テンプレートとは

「部分テンプレート」は、複数のページで共通して利用できるViewファイルのことです。
複数のファイルで重複しているコードを一つのファイルにまとまることで、修正時に一箇所だけの修正で済んだり、コードの記述が少なくなり可読性が高くなります。

手順はざっくりこんな感じです。
1.共通化できる部分を探し出す
2.部分テンプレートファイルを作成
3.部分テンプレートファイルに共通化部分を記述
4.部分テンプレートファイルを呼び出す

1.共通化できる部分を探し出す

そのままですが、重複している箇所を探す作業です。

2.部分テンプレートファイルを作成

ファイル名の先頭にアンダースコア( _ )付きのerbファイルが、部分テンプレートファイルとして認識されます。今回は投稿機能を部分テンプレート化で見ていきます。
app/views/books/_newform.html.erb

3.部分テンプレートファイルに共通化部分を記述

共通部分を切り出し、ファイルに貼り付けます。
基本的に部分テンプレートファイルではローカル変数(@がないやつ)を使います。
部分テンプレートファイル内でインスタンス変数(@がついてるやつ)を利用すると、controller側でインスタンス変数の名前や挙動を変更したとき、部分テンプレート側も変更しなければいけなくなるからです。

app/views/books/_newform.html.erb
<%= form_for(book) do |f| %>
    <div class="field row">
        <%= f.label :title %><br>
        <%= f.text_field :title, class: "col-xs-3 book_title" %>
    </div>
    <p></p>

    <div class="field row">
        <%= f.label :body %><br>
        <%= f.text_area :body, class: "col-xs-3 book_body" %>
    </div>

    <div class="actions row">
        <%= f.submit class: "btn btn-primary col-xs-3" %>
    </div>
<% end %>
</div>

4.部分テンプレートファイルを呼び出す

呼び出す時の書き方は以下の通りです。
なお、呼び出すときは部分テンプレートのアンダースコアは省略します。

<%= render [部分テンプレートファイルの指定], [ローカル変数]:[渡す値] %>
app/views/books/index.html.erb
<%= render 'books/newform', book: @book %>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む