Tag rails

在 Trailblazer 的 Policy 中透過客制 Exception 來處理複雜的「錯誤回應」

為了盡量貼近 Trailblazer 的設計概念,許多原先會透過 Controller before_action 去處理的權限管控,盡量都搬進 Trailblazer 的 Policy 裡。 Policy 採用類似 Pundit 的語法,典型的 Policy 如下: class Thing::Policy def initialize(user, thing) @user, @thing = user, thing end def create? (admin? || approved?) && @thing.persisted? end private def admin? @user.admin == true end def approved? @user.is_approved end end 在 Operation 中若要調用這隻 Policy 的話要宣告: class Thing::Create < Trailblazer::Operation builds -> (params) { dispatched_class_accroding_to(params) } def self.dispatched_class_accroding_to(params) Thing::Create end include Trailblazer::Operation::Policy policy Thing::Policy, :create?

初探 Trailblazer 框架

最近公司的 Rails 專案試用了 Trailblazer 這套整理 Rails 程式碼的框架。(目前使用的是 1.1 版) Trailblazer 是擺在 Rails 上的一套工具,雖然有本專書可以翻找,但實際上就是個自成一格的整理程式碼套路,除了要學新的 DSL 外,在 API 文件沒有寫得非常詳細,加上使用者也沒多到可以 stackoverflow 的情況下,假如開發需求和設計者 Nick Sutterer 設想的情況不一樣時,小小撞牆是難免的。 Trailblazer 是模組化設計,所以並非所有模組需要安裝才能開始享受 Trailblazer 帶來的方便感,除了主要的 Operation / Policy / Contract / Cell 等稍微有摸過外,其他的眉眉角角尚待開發。稍微整理一下截至目前為止使用這套框架的個人認知,這套 Trailblazer 的目的是將原先 Rails 單純 MVC 架構下可能會散亂在 Controller / Model 層的商業邏輯,集中起來包在 Operation 的概念裡,(在理想狀況下)分別簡化日益肥大的 Controller / Model / View 邏輯: Controller 讓 controller 單純負責傳送 current_user / resource 到正確的 operation 並視操作結果導向相對應的路由 權限控管交給 Policy 處理,用 controller 塞給 operation 的 current_user 配合 operation 的 model!

利用 find_in_batches 在 Rails 做跨資料庫的資料移轉操作

在處理跨資料庫搬動並且需要一筆一筆資料去做各種運算處理時,最直覺的方式,是把表格裡的所有資料通通透過 ORM 撈到 ActiveRecord::Association 陣列裡,再用 each 去做操作: User.all.each do |user| user.email = "assign@new.email" ... end 但由於 Rails 會需要先去建立這些 objects,如果資料筆數很多的話,一次通通撈出來會佔用過多不必要的記憶體,可以使用 find_in_batches 做批次處理,減少系統 RAM 的負擔。 User.find_in_batches do |group| group.each do |user| user.email = "assign@new.email" ... end end 但如果要做跨資料庫的處理時,需要特別安插 ActiveRecord::Base.establish_connection 讓 Rails 知道當前操作需要連到哪個資料庫,舉例而言,假如想把資料一批一批從 :origin 調出來,然後檢查、編輯以後塞到 :migrated 資料庫裡,那麼需要在 Query 前去告訴 Rails 目前要連到哪個資料去撈資料: # Tell rails to get data from :origin ActiveRecord::Base.establish_connection :origin User.find_in_batches do |group| group.each do |user| # user variable here are the user object that Rails queried from :origin DB and initialed # Tell rails to switch connection to :migrated for data manipulation ActiveRecord::Base.

使用 Google Sheets API 拉資料到 Rails APP 裡

Google Sheets 很好用,一些程式中需要跨部門討論的字串定義,又或者是需要讓不方便存取原始碼的同事也能清楚知道原始碼裡部分設定字串的時候,通常都可以利用 Google Sheet 來做討論平台,再以 Google Sheets 上面的文件為基礎,匯入主程式中做各種處理,確保主程式的內容與 Google Sheets 上的內容同步。 要匯入 Google Sheets 內容到程式裡最簡單的方式,是輸出 csv 後寫個小 script 去讀取 csv 資料,轉成 Array 後就能靈活使用。 但若是開發過程當中常常要不斷更新 Google Sheets 欄位後再手動匯出 csv 跑 script 去後處理,這種半自動的方式還是令人覺得太過麻煩。可以考慮進一步用 Google Sheets API 去拉特定 Google Sheets 工作表資料,把半自動的「到瀏覽器視窗匯出 csv > 儲存檔案到特定資料夾 > 執行 script 完成匯入」簡化為「執行 script 完成匯入」。 首先到 Google API Console 申請一個可用的 API 身份,因為這隻 API 只要存取特定 Google Sheets 的內容,與其他 Google User 沒有互動,所以選用 Service Account 的授權方式就可以。 如果你的 Google 帳號之前沒申請過 Google API 的話,要先「建立專案」。

在 rails 上實作轉移 Parse 手機推播服務到 Amazon SNS

簡要說明 為了避免使用者因服務轉換而被迫強制更新,移轉過程採用漸進方式。 如果不需要如此痛苦的同時使用兩種服務的捧油,可以參考這一篇的實作,直接用 AWS Mobile Hub 的服務做批次資料移轉。 由於在轉換期需要同時支援兩邊服務運作,又要避免使用者重複收到相同的推播,需要實作「對同一裝置擇一服務進行推播」。本篇實作的概念是,使用者只有在用新版本的 APP 登入時,伺服器才會從 APP 取得 device_token 去註冊 Amazon SNS endpoint_arn,同時刪除 Parse 服務上的使用者的所有 Installation 紀錄。 Parse 跟 Amazon SNS 的 API 邏輯有些不同,主要差別在於,Parse 只需要在同一 request 裡就可以達到「推播給符合某條件的 channels 組合,並且可以指定其中的特定 users 組合」,但是 Amazon 的推播就是 topic_arn / target_arn 兩種,要收到 topic_arn 的話要先讓 endpoint_arn 訂閱特定的 topic_arn,而 target_arn 則是一個 request 只能推播給單一 target_arn (endpoint_arn)。 開發步驟 打包一隻 AmazonSnsWrapper 把 Amazon SDK 提供的 method 客製成符合需求的介面,專門負責跟 Amazon server 溝通。 新增 AmazonSnsInfo Model 以記錄使用者的 SNS 訂閱狀況。 新增 ParseInstallationWrapper 打包 Parse RESTful API 裡的 method,查詢使用者之前透過 APP 註冊的 Installations,並且刪除已經成功轉換到 Amazon SNS 的 Installation。 新增 update_push_subscription / logout API 來針對 APP 傳送的 device_token 做各種 push subscription / unsubscription 的操作。 新增 PushServicesWrapper 把所有 Push 行為的邏輯打包在一起方便做服務切換。 一、 AmazonSnsWrapper 首先,我們得先建好 Amazon SNS 服務中 endpoint_arn / topic_arn / subscription_arn 新增、查詢、訂閱與刪除等機制,我習慣把第三方 API 的互動包成一個 Ruby Class 方便測試跟管理,建一個 AmazonSnsWrapper 來打包 SDK 相關的程式碼。

Rails grape API 上實作 JWT 多重登入

多重登入的需求,在服務本身允許「跨裝置體驗」或者是「接受不同 request agent (APP request / mobile browser)」的時候顯得重要。 理想情況下,可以讓使用者在介面上面清楚知道目前帳號的登入狀態,並在登入數超過服務上限的時候,讓使用者自己選擇要移除登入授權的裝置,使用者流程大概會像(這篇文)那樣,要做到類似參考連結這樣的管理介面,需要我目前還不會的前端的配合建置與基本的 session 管理 API。本篇只先建置允許使用多重登入的資料結構與登入驗證 API。 選擇用 JWT (Jason Web Token) 作為主要的憑證溝通,優點有一些: 透過加密 payload 來比對資料庫內容的方式,可以不用在資料表中「直接存入」與憑證相關的訊息,加密跟解密只要透過協定就能進行調整。 payload 裡面塞入的訊息可以自訂,在各服務跟裝置之間交換加密過後的使用者資料。 signing key / protocol 可以在有資安疑慮時更換。 使用 Devise 的 Rails 捧油也可參考 devise_token_auth 這個 gem,整合效果在文件上面看起來很完善,但因為目前我手邊的系統絕大多數的 request 都來自 grape API endpoint ,比較過後,跟搞懂並駕馭 devise 設定比起來,我還是選擇自刻 JWT 較節省開發時間。 在 Rails grape API 上實作 JWT 多重登入,分 4 個階段。 建立 user_sessions 資料表跟 UserSession Model Setup,目前找得到的 Rails JWT 整合教學大多是用在 Controller / Rails View 上,為了之後如果要導入前端管理裝置時可以很方便地去刪除跟紀錄不要的 Session,我選擇了透過 UserSession 這個 model 來做媒介,之後要新增 API 的時候直接去對這個 table 做 query 跟各種欄位處理,盡量把 Session 管理跟目前龐大的 users table 切乾淨。 寫一隻自己的 JasonWebToken Service 把 jwt 的 code 包裝得更適合自己的系統使用:雖然 jwt 這隻 gem 的語法已經寫得很美,但還想更方便地只要把 headers 塞進去這個 wrapper 就可以得到 user object 回傳或者是指定的錯誤訊息。 做好資料結構跟 JasonWebToken Service 後,就可以開始使用準備好的工具來更換 Grape API 的登入跟檢查憑證機制,首先要在登入的 API output 提供 jwt 字串。 最後,在檢查憑證機制的 helper 裡使用 JasonWebToken class 去 decode request 裡面的 jwt。

Rails 中使用 Paperclip 存 URI 附件

使用 Rails APP + Paperclip Gem 做附件存取系統,靜態檔案的資料夾格式預設是跟著 id 跑( /000/000/000/ 九碼化的 ID 切成 3 階資料夾),最近因為要做資料庫合併,大批的資料會被賦予新的 ID,原先放在 AWS S3 上面的靜態資料夾結構就會 mapping 錯誤,必須要依照新的 ID 去存放相對應的資料夾結構。 由於部分圖片附件在存取時還會同步進行縮圖,如果要用 s3-cmd 這類的外部工具直接搬資料,得連各種不同 model 的縮圖定義一起處理,痛苦指數不低。 原先預計要從 HTTP GET request 的 response body 裡直接塞 tempfile : url = 'https://s3.amazonaws.com' path = '/BUCKET-NAME/MODEL/ATTACHMENT/000/016/222/original/FILE.jpg' conn = Faraday.new(:url => url) do |faraday| faraday.request :url_encoded faraday.response :logger faraday.adapter Faraday.default_adapter end response = conn.get path model.attachment = response.body 一直噴出各式各樣無法存入或者檔案格式錯誤的錯誤訊息。 試過自行重組檔案的檔頭 : attachment_file = { :filename => /^.