Tìm hiểu GraphQL (Phần 3): GraphQL with Rails
Chào các bạn, Ở bài viết trước mình đã giới thiệu qua về các khái niệm cơ bản và tư tưởng chủ đạo của GraphQL Tuy nhiên tất cả vẫn dừng ở mức khái niệm về cách tạo queries, cách update dữ liệu bằng mutations, cách tạo schema để chỉ định các khả năng của API và xác định cách client có thể yêu ...
Chào các bạn,
Ở bài viết trước mình đã giới thiệu qua về các khái niệm cơ bản và tư tưởng chủ đạo của GraphQL
Tuy nhiên tất cả vẫn dừng ở mức khái niệm về cách tạo queries, cách update dữ liệu bằng mutations, cách tạo schema để chỉ định các khả năng của API và xác định cách client có thể yêu cầu dữ liệu....mà chưa chỉ ra được cách xử lí dữ liệu ở server như thế nào
Mình sẽ bắt đầu với một ví dụ rất cơ bản đó là Đăng ki, Đăng nhập và Đăng xuất người dùng.
Như đã nói ở bài trước, do GraphQL chỉ là một đặc tả, bạn có thể dùng nó với bất cứ thư viện hay nền tảng nào. Ở bài viết này mình sẽ áp dụng GraphQl vào một dự án Rails
Cài đặt GraphQL
Thêm vào Gemfile và bundle
gem "graphql"
Xong rồi chạy:
rails generate graphql:install
Kết quả sẽ tạo ra 1 thư mục app/graphql, và chúng ta chủ yếu sẽ làm việc ở đây
Ngoài ra lệnh trên sẽ tạo ra 1 controller tên là GraphqlController và thay đổi 1 chút ở routes file, các bạn có thể mở ra xem (hehe)
Xong rồi, bắt đầu nào...
Chuẩn bị một số thứ
Giả sử bạn đang có một project Rails, và có một số models cơ bản phục vụ cho việc xác thực người dùng như sau:
Model User
class User < ApplicationRecord has_secure_password validates :name, presence: true validates :email, presence: true, uniqueness: true has_one :auth_token end
Model UserToken
class AuthToken < ApplicationRecord belongs_to :user validates :token, presence: true validates :refresh_token, presence: true validates :expired_at, presence: true class << self def generate! user token = find_or_initialize_by user: user token.renew! end def verify token find_by(token: token)&.user end end def renew! // Dùng để tạo mới token end end
Minh họa thôi nhé
Nhảy vào GraphQL nào
Tạo người dùng
Tạo GraphQL type để biểu diễn một người dùng:
/graphql/types/user_type.rb
Types::UserType = GraphQL::ObjectType.define do name "User" field :id, !types.ID field :name, !types.String field :email, !types.String end
Bây giờ chúng ta có model User và GraphQL type của nó là "UserType" với tên là "User" và các trường bắt buộc: id, name, email
Như mình đã nói các loại Query, Mutation và Subscription là các entry points cho các request được gửi bởi client. Vì thế nên mình sẽ có các QueryType, MutationType gồm các fields là các root-field của các request tương ứng là query hay mutation
Nhớ lại bài trước, mình đã có một ví dụ về mutation để tạo người dùng là "createUser", bây giờ xây dựng nó nào.
Tất cả các GraphQL mutation bắt đầu từ một root tpye được gọi là Mutation.
/graphql/types/mutation_type.rb
Types::MutationType = GraphQL::ObjectType.define do name "Mutation" field :createUser, Types::UserType do description "Create User" argument :name, !types.String argument :email, !types.String argument :password, !types.String resolve ->(_obj, args, _ctx) { User.create! name: args[:name], email: args[:email], password: args[:password] } end end
Ở trên mình đã tạo ra một MutationType có một field là createUser
- Types::UserType chỉ định type sẽ được trả về
- description: Mô tả cho mutation
- argument: Chỉ định các đối số, sử dụng "!" bắt buộc phải có mặt
- types.String là types định nghĩa sẵn của GraphQL, chỉ định kiểu dữ liệu của argument
- resolve: hàm xử lí dữ liệu, một hàm resolve nhận vào ba đối số:
- obj: Đối tượng trước đó, đối với một field trên root Query types thường không được sử dụng.
- args: Các đối số được cung cấp cho field
- ctx: Một giá trị được cung cấp cho tất cả resolver và chứa thông tin ngữ cảnh quan trọng như người dùng hiện đang đăng nhập, hoặc truy cập vào cơ sở dữ liệu....
Những cái này lúc nào gặp mình sẽ nói thêm (hehe), ở trên đây thì mình đã dùng thằng args rồi
Chạy thử nào!
(done)
Tuy nhiên có một cách để viết resolve nữa đó là sử dụng GraphQL::Function
Lúc đó "createUser" sẽ đơn giản nhưu thế này:
/graphql/types/mutation_type.rb
Types::MutationType = GraphQL::ObjectType.define do name "Mutation" field :createUser, function: Resolvers::Users::CreateUser.new end
và class "Resolvers::Users::CreateUser" sẽ như sau:
/graphql/resolvers/users/create_uer.rb
class Resolvers::Users::CreateUser < GraphQL::Function argument :name, !types.String argument :email, !types.String argument :password, !types.String type Types::UserType def call _obj, args, _ctx User.create! name: args[:name], email: args[:email], password: args[:password] end end
Dể đọc dể hiểu hơn đúng không (hehe)
Như vậy là xong phần Đăng kí User rồi nhé (tất nhiên chỉ là ví dụ minh họa :v :v)
Đăng nhập
Có người dùng rồi thì cho nó đăng nhập thôi :v
Đăng nhập thì đơn giản là nhập email và password, đúng thì trả về token để tiếp tục dùng API, mà nó cũng có thay đổi một số dữ liệu nên sẽ dùng mutation
Thêm vào MutationType:
/graphql/types/mutation_type.rb
Types::MutationType = GraphQL::ObjectType.define do name "Mutation" field :createUser, function: Resolvers::Users::CreateUser.new field :signInUser, function: Resolvers::Auth::SignInUser.new end
Như vậy là đã có một mutation là "signInUser"
Viết tiếp Resolver để xử lí thôi
/graphql/resolvers/auth/sign_in_uer.rb
class Resolvers::Auth::SignInUser < GraphQL::Function argument :email, !types.String argument :password, !types.String # Định nghĩa luôn kiểu trả về cho mutation này, ko dùng những cái đã có type do name 'SigninPayload' field :token, types.String field :user, Types::UserType end def call _obj, args, ctx email = args[:email] password = args[:password] # basic validation return unless email && password user = User.find_by email: email return unless user return unless user.authenticate password AuthToken.generate! user ctx[:session][:token] = user.auth_token.token OpenStruct.new({ user: user, token: user.auth_token.token }) end end
Tất nhiên code signin ở đây cũng chỉ mang tính chất minh họa nha (yaoming)
Không có gì đặc biệt, ngoại trừ có sự xuất hiện của thằng "ctx", tí mình sẽ nói tới nó sau :v
Thử đăng nhập cái coi
Với kiểu trả về tự định nghĩa ở trên thì mình có thể yêu cầu dữ liệu trả về là token và các thông tin tùy ý của user
Đăng xuất
Đăng nhập được rồi, giờ cho nó đăng xuất thôi :v
À mà trước khi đăng xuất thì phải có điều kiện là phải đăng nhập rồi, vậy thì làm sao để check xem user đã đăng nhập hay chưa?
Đã đến lúc dùng thèn ctx rồi:
Nhắc lại tí:
ctx: Một giá trị được cung cấp cho tất cả resolver và chứa thông tin ngữ cảnh quan trọng như người dùng hiện đang đăng nhập, hoặc truy cập vào cơ sở dữ liệu....
Là sao (??) Thôi hiểu đơn giản thì nó như là session của GraphQL, nó dùng để chia sẽ dữ liệu giữa các Resolvers
Rất đơn giản, bạn chỉ cần sửa lại cái GraphqlController đã nói ở trên như sau:
app/controllers/graphql_controller.rb
class GraphqlController < ApplicationController skip_before_action :verify_authenticity_token def execute variables = ensure_hash params[:variables] query = params[:query] operation_name = params[:operationName] context = { session: session, current_user: current_user } result = NPlusOneSchema.execute query, variables: variables, context: context, operation_name: operation_name render json: result end private def ensure_hash ambiguous_param .... end def current_user return unless token = session[:token] AuthToken.verify token end end
Vẫn là mang tính chất minh họa thôi nhé :v
À mà lúc trước có nói là có thay đổi 1 chút chi đó ở routes file, ngó qua cái nào
config/routes.rb
Rails.application.routes.draw do if Rails.env.development? mount GraphiQL::Rails::Engine, at: "/graphiql", graphql_path: "/graphql" end post "/graphql", to: "graphql#execute" ....... end
Quá rõ ràng rồi, với mỗi request gửi lên thì nó đều chạy vô cái hàm execute của GraphqlController và sau đó là NPlusOneSchema.execute và truyền vào thằng context, thì hiển nhiên thằng Resolver nào cũng dùng cái context đó rồi
À, cái NPlusOneSchema là gì hả (??) nó là cái schema mà mình đã giới thiệu ở bài trước đó, tí mình sẽ nói thêm, còn cái tên nó thì...tại mình dùng lại cái project cũ nên tên nó bựa thế đó (hehe)
Quay lại cái thằng Resolver "signInUser" ở trên bạn sẽ thấy cái này
ctx[:session][:token] = user.auth_token.token
Tất nhiên cái này cũng chỉ mang tính chất minh họa thôi chứ chả ai làm thế này (yaoming)
Thường token người ta có thể gửi lên theo params hoặc là trên headers của request, nhưng ở đây mình ví dụ lưu vào nó session để xem hoạt động của thằng "ctx" luôn
Hiểu đơn giản là khi đăng nhập thành công thì mình bốc thằng session trong ctx ra rồi nhét cái token vào đó (hơi bựa :v), vì trong Resolver nó không gọi trực tiếp session được .
Và để kiểm tra có user đăng nhập hay chưa thì mình chỉ đơn giản là bốc lại cái token từ trong session ra, rồi kiếm thằng user tương ứng rồi lại quăng thằng user đó vào cái context (như đã nói, token có thể lấy từ chổ khác hay hơn)
Resolver có yêu cầu authen thì chỉ cần kiểm tra trong ctx có thằng user nào không là được
/graphql/resolvers/base.rb
class Resolvers::Base < GraphQL::Function def authenticate! ctx raise GraphQL::ExecutionError.new("Authentication required") if ctx[:current_user].blank? end end
Và tất nhiên, sửa lại các Resolver trên kế thừa từ "Resolvers::Base" nhé
/graphql/resolvers/auth/sign_out_user.rb
class Resolvers::Auth::SignOutUser < Resolvers::Base type types.String def call _obj, args, ctx authenticate! ctx ctx[:current_user].auth_token&.destroy! ctx[:current_user] = nil ctx[:session][:token] = nil :ok end end
Vẫn là minh họa thôi nhé
Đăng xuất thêm phát nữa:
(done)
Schema
Hốt tất cả Queries và Mutations vào Schema nhé, vì nó execute ở đây mà (bỏ qua cái tên đi :v)
app/graphql/n_plus_one_schema.rb
NPlusOneSchema = GraphQL::Schema.define do mutation Types::MutationType query Types::QueryType end
Như vậy đã xong
Hẹn gặp lại các bạn ở bài viết sau
Tham khảo: https://www.howtographql.com