We want to put user interactions into a relational database
As long as those needs sound like CRUD
💁♂️BOB: I want to give Peter access to the system
🤖REST: POST /users params: {user: {first_name: "Peter", last_name: "Parker"}}
💁♂️BOB: Peter got married! Let's update his last name
🤖REST: PATCH /users/1 params: {user: {last_name: "Watson"}}
Familiar-looking REST controller
class UsersController < ApplicationController
def update
user = User.find(params[:id])
user.update(user_params)
end
private
def user_params
params.require(:user).permit(:first_name, :last_name)
end
end
💁♂️BOB:Peter's a straight-shooter with upper management written all over him. I'd like to promote Peter to manager. Managers should automatically get certain permissions
🤖REST:PATCH /users/1 params: {user: {role: "manager"}}
class UsersController < ApplicationController
def update
user = User.find(params[:id])
if params["user"]["role"] && current_user.admin?
user.update(role: params["users"]["role"])
user.permissions.create(
{name: "Access To Nice Coffee Machine"}
)
end
user.update(user_params)
end
private
def user_params
params.require(:user).permit(:first_name, :last_name)
end
end
We use new REST resources to solve problems like this!
class UserPromotionsController < ApplicationController
# POST /users/#{user_id}/promote
def create
user = User.find(params[:user_id])
if current_user.admin?
user.update(role: "admin")
user.permissions.create(
{name: "Access To Nice Coffee Machine"}
)
end
end
end
Resources that are not be backed by a database table. It's a way to accommodate changes that aren't 1:1 with database tables
REST contortions still attempt to make user actions look like database operations.
Users want to promote other users, not insert user promotions
POST /users/promote_#{bob}_to_admin
POST /api/graphql
module Types
class MutationType < Types::BaseObject
field :add_user, mutation: Mutations::AddUserMutation
field :update_user, mutation: Mutations::UpdateUserMutation
field :promote_user, mutation: Mutations::PromoteUserMutation
end
end
module Mutations
class ChangeRoleMutation < BaseMutation
argument :user_id, ID
field :user, Types::UserType
field :errors, [ErrorData], null: false
def resolve(user_id)
user_promotion = UserPromotion.new(
current_user: context[:current_user],
user_id: user_id
)
user_promotion.perform!
{
user: user_promotion.user,
errors: user_promotion.errors
}
end
end
end
class UserPromotion
extend ActiveModel::Naming
attr_accessor :current_user, :user
attr_reader :errors
def initialize(current_user:, user_id:)
@errors = ActiveModel::Errors.new(self)
@current_user = current_user
@user = User.find(user_id)
end
def perform!
if permitted?
user.update!(role: "admin")
user.permissions.create(
{name: "Access To Nice Coffee Machine"}
)
else
errors.add(:current_user, "cannot promote other users")
end
end
private
def permitted?
current_user.admin?
end
end
REST is great, but starts to break when user interactions aren't 1:1 with database operations
graphql has good answers if you are building an API + client(s) application