Servicer

NOTE: Servicer is NOT a service object.

Servicer contains a collection of CRUD operations that can be overridden and tailered to the needs.

First of all, it’s always recommended to create a base servicer class Admin::ApplicationServicer as below, so that devs can have better control of developing global changes/functions:

# app/servicers/admin/application_servicer.rb
class Admin::ApplicationServicer < Wallaby::ModelServicer
end

This base servicer can be generated by Wallaby generator as well:

rails g wallaby:install

See Mapping - Servicer for the configuration if Admin::ApplicationServicer is taken for other purpose.

Starting with:

Configuration can be set for:

Accessing helper methods:

  • #user - accessing user object.
  • #authorizer - accessing authorizer which provides functions for authorization.

Customizing CRUD operations:

  • permit - whitelisting params for mass assignment.
  • collection - returning records from data source.
  • paginate - paginating records.
  • new - initializing the new resource object for form to use.
  • find - finding the record.
  • create - creating record.
  • update - updating record.
  • destroy - deleting record.

Declaration

Read more at Servicer Naming Convention

Let’s see how a servicer can be created so that Wallaby knows its existence.

Similar to the way in Rails, create a custom servicer for model Product inheriting from Admin::ApplicationServicer (the base servicer mentioned above) as below:

# app/servicers/product_servicer.rb
class ProductServicer < Admin::ApplicationServicer
end

If ProductServicer is taken, it is still possible to use another name (e.g. Admin::ProductServicer). However, the attribute model_class must be specified. See model_class for examples.

.base_class!

All servicers will be preloaded and processed by Wallaby in order to build up the mapping between servicers and models. If the servicer is considered not to be processed, it can be flagged by using base_class!:

# app/servicers/admin/special_servicer.rb
class Admin::SpecialServicer < Admin::ApplicationServicer
  base_class!
end

.model_class

According to Wallaby’s Servicer Naming Convention, if a custom servicer cannot reflect the association to the correct model, for example, as Admin::ProductServicer to Product, it is required to specify the model class in the servicer as below:

# app/servicers/admin/product_servicer.rb
class Admin::ProductServicer < Admin::ApplicationServicer
  self.model_class = Product
end

Helper Methods

#user

It’s the reference of wallaby_user from controller. To access user, it goes:

# app/servicers/admin/product_servicer.rb
class Admin::ProductServicer < Admin::ApplicationServicer
  self.model_class = Product

  def send_promotion_email
    ProductMailer.with(user: user).promotion_email.deliver_later
  end
end

#authorizer

It’s the instance of Model Authorizer that does authorization for the model and the action

CRUD

permit

This is the template method to whitelist parameters for mass assignment automatically. It works differently for different ORMs, for example:

  • ActiveRecord

    NOTE: as described below, Wallaby doesn’t automatically permit nested attribute params for has_one associations.

    # app/models/product.rb
    class Product < ActiveRecord::Base
      has_one :product_detail
      has_one :picture, as: :imageable
      has_many :order_items, class_name: Order::Item.name
      has_many :orders, through: :order_items
      belongs_to :category
      has_and_belongs_to_many :tags
    end
    
    # app/servicers/admin/product_servicer.rb
    class Admin::ProductServicer < Admin::ApplicationServicer
      self.model_class = Product
    end
    
    product_servicer.permit params, 'create'
    product_servicer.permit params, 'update'
    # both will work like =>
    # params.require(:product).permit(
    #   "sku", "name", "description",
    #   "category_id",
    #   "order_item_ids"=>[], "order_ids"=>[], "tag_ids"=>[]
    # )
    

Please keep in mind that when servicer permits parameters, its behavior is affected by authorizer’s permit_params as well.

  • For example, if pundit policy has defined following permitted_attributes:

    # app/policies/product_policy.rb
    class ProductPolicy < ApplicationPolicy
      def permitted_attributes
        if user.admin? || user.owner_of?(product)
          [:sku, :name, :category_id]
        else
          [:description]
        end
      end
    end
    

    Then servicer will only allow what policy has allowed instead of the general fields.

To customize what params should be permitted, it goes:

  • if utilizing what Wallaby has implemented:

    # app/servicers/admin/product_servicer.rb
    class Admin::ProductServicer < Admin::ApplicationServicer
      self.model_class = Product
    
      def permit(params, action)
        permitted = super
        action == 'create' ? permitted : permitted.slice(:name)
      end
    end
    
  • or simply replacing this template method:

    # app/servicers/admin/product_servicer.rb
    class Admin::ProductServicer < Admin::ApplicationServicer
      self.model_class = Product
    
      def permit(params, action)
        if action == 'create'
          params.require(:product).permit(:sku, :name, product_detail_attributes: [:meta_data])
        else
          authorizer.permit_params action, model_class
        end
      end
    end
    

collection

This is the template method that queries data source and returns a collection of data. For ActiveRecord, it handles the following parameters:

  • params[:q]: an expression to perform search (see Colon Search for more information).
  • params[:sort]: expression (e.g. name asc,updated_at desc) to perform sorting.
  • params[:filter]: the filter name (e.g. red) that Wallaby should apply its scope (e.g. where color: red) to the search

Please keep in mind that the query that servicer carries out is also filtered by authorizer’s accessible_for method.

  • For example, if ability has defined following condition:

    # app/models/ability.rb
    class Ability
      include CanCan::Ability
    
      def initialize(user)
        can :index, Product, active: true
      end
    end
    

    Then the scope active: ture will be applied to query as well.

To customize how query should be carried out, it goes:

  • if utilizing what Wallaby has implemented:

    # app/servicers/admin/product_servicer.rb
    class Admin::ProductServicer < Admin::ApplicationServicer
      self.model_class = Product
    
      def collection(params)
        super.where active: true
      end
    end
    
  • or simply replacing this template method:

    # app/servicers/admin/product_servicer.rb
    class Admin::ProductServicer < Admin::ApplicationServicer
      self.model_class = Product
    
      def collection(params)
        if params[:active]
          Product.where active: true
        else
          authorizer.accessible_for :index, Product.where(nil)
        end
      end
    end
    

paginate

This is the template method that handles pagination for a query. For ActiveRecord, kaminari is used to handle pagination with the following params:

  • params[:page]: page number for pagination.
  • params[:per]: page size for pagination.

To customize how query should be paginated, it goes:

  • if utilizing what Wallaby has implemented:

    # app/servicers/admin/product_servicer.rb
    class Admin::ProductServicer < Admin::ApplicationServicer
      self.model_class = Product
    
      def paginate(query, params)
        super.per 100
      end
    end
    
  • or simply replacing this template method:

    # app/servicers/admin/product_servicer.rb
    class Admin::ProductServicer < Admin::ApplicationServicer
      self.model_class = Product
    
      def paginate(query, params)
        query.page(params[:page_number]).per(params[:page_size])
      end
    end
    

new

This is the template method that builds the resource instance for creation form page.

To customize how resource instance should be initialized, it goes:

  • if utilizing what Wallaby has implemented:

    # app/servicers/admin/product_servicer.rb
    class Admin::ProductServicer < Admin::ApplicationServicer
      self.model_class = Product
    
      def new(params)
        super.tap do |resource|
          resource.active = true
        end
      end
    end
    
  • or simply replacing this template method:

    # app/servicers/admin/product_servicer.rb
    class Admin::ProductServicer < Admin::ApplicationServicer
      self.model_class = Product
    
      def new(params)
        Product.new(params.merge active: true)
      end
    end
    

find

This is the template method that finds the record from data source.

To customize how resource should be found, it goes:

  • if utilizing what Wallaby has implemented:

    # app/servicers/admin/product_servicer.rb
    class Admin::ProductServicer < Admin::ApplicationServicer
      self.model_class = Product
    
      def find(id, params)
        super.tap do |resource|
          resource.active = true
        end
      end
    end
    
  • or simply replacing this template method:

    # app/servicers/admin/product_servicer.rb
    class Admin::ProductServicer < Admin::ApplicationServicer
      self.model_class = Product
    
      def find(id, params)
        Product.friendly.find id
      end
    end
    

create

This is the template method that create the resource and save to data source.

Please keep in mind that the attributes will be filtered by authorizer’s attributes_for method.

  • For example, if ability has defined following condition:

    # app/models/ability.rb
    class Ability
      include CanCan::Ability
    
      def initialize(user)
        can :index, Product, active: true
      end
    end
    

    Then the attribute active: ture will be applied to resource before saving.

To customize how resource should be created, it goes:

  • if utilizing what Wallaby has implemented:

    # app/servicers/admin/product_servicer.rb
    class Admin::ProductServicer < Admin::ApplicationServicer
      self.model_class = Product
    
      def create(resource, params)
        resource.active = true
        super
      end
    end
    
  • or simply replacing this template method:

    # app/servicers/admin/product_servicer.rb
    class Admin::ProductServicer < Admin::ApplicationServicer
      self.model_class = Product
    
      def create(resource, params)
        ensure_attributes_for authorizer, :create, resource
        resource.save if valid? resource
        resource
      end
    end
    

update

This is the template method that update the resource and save to data source.

Please keep in mind that the attributes will be filtered by authorizer’s attributes_for method.

  • For example, if ability has defined following condition:

    # app/models/ability.rb
    class Ability
      include CanCan::Ability
    
      def initialize(user)
        can :index, Product, active: true
      end
    end
    

    Then the attribute active: ture will be applied to resource before saving.

To customize how resource should be updated, it goes:

  • if utilizing what Wallaby has implemented:

    # app/servicers/admin/product_servicer.rb
    class Admin::ProductServicer < Admin::ApplicationServicer
      self.model_class = Product
    
      def update(resource, params)
        resource.active = true
        super
      end
    end
    
  • or simply replacing this template method:

    # app/servicers/admin/product_servicer.rb
    class Admin::ProductServicer < Admin::ApplicationServicer
      self.model_class = Product
    
      def update(resource, params)
        ensure_attributes_for authorizer, :update, resource
        resource.save if valid? resource
        resource
      end
    end
    

destroy

This is the template method that destroy the resource from data source.

To customize how resource should be destroyed, it goes:

  • if utilizing what Wallaby has implemented:

    # app/servicers/admin/product_servicer.rb
    class Admin::ProductServicer < Admin::ApplicationServicer
      self.model_class = Product
    
      def destroy(resource, params)
        super.tap do
          audit_log :destroy, resource
        end
      end
    end
    
  • or simply replacing this template method:

    # app/servicers/admin/product_servicer.rb
    class Admin::ProductServicer < Admin::ApplicationServicer
      self.model_class = Product
    
      def destroy(resource, params)
        unregister resource
        resource.destroy
      end
    end