Skip to content

Overview

Across the app, model manipulations are exposed as resources with standard rest api actions. Keeping with principles of DRY, all of the controllers extend resources controller to provide CRUD operations.

Api::V1::ResourcesController

The Api::V1::ResourcesController is a base controller that provides common actions and methods for managing resources via the API. It includes actions for CRUD operations, batch operations, and search functionalities. This controller is designed to be subclassed by specific resource controllers.

Why is it needed?

The Api::V1::ResourcesController is needed to provide a standardized way of handling resource operations across the application. By centralizing common actions and methods in a base controller, it ensures consistency and reduces code duplication. This approach also makes it easier to maintain and extend the functionality of resource controllers.

What functionalities does it provide across the app?

The Api::V1::ResourcesController provides the following functionalities:

  • CRUD Operations: Standard actions for creating, reading, updating, and deleting resources.
  • Batch Operations: Actions for creating and deleting multiple resources in a single request.
  • Search and Filter: Actions for searching and filtering resources using Ransack and custom search filters.
  • Pagination: Methods for applying pagination to resource collections.
  • Authorization: Methods for authorizing resource actions using Pundit policies.
  • Helper Methods: Utility methods for handling resource operations, such as new_resource, batch_new_resources, requested_resource, apply_ransack, apply_order, apply_pagination, resource_scope, resource_class, permitted_params, batch_permitted_params, json_attributes, authorize_search, and authorize_resource.

Methods

index

The index action retrieves a paginated list of resources. It applies authorization, ordering, and pagination to the resource scope.

def index
  authorize_resource(resource_class)
  resources = apply_order(resource_scope)
  resources = apply_pagination(resources)
  data = response_as(:paginated, scope: resources, json_attributes:)
  status_code = :ok
  render json: data, status: status_code
end

show

The show action retrieves a single resource by its ID. It applies authorization and returns the resource data if found, otherwise returns a not found response.

def show
  resource = requested_resource
  if resource.present?
    authorize_resource(resource)
    data = response_as :success, data: resource.as_json(json_attributes)
    status_code = :ok
  else
    data = failure_response(:id, "Not Found")
    status_code = :not_found
  end
  render json: data, status: status_code
end

create

The create action creates a new resource with the permitted parameters. It applies authorization and returns the created resource data if successful, otherwise returns validation errors.

def create
  resource = new_resource
  authorize_resource(resource)
  if resource.save
    data = response_as :success, data: resource.as_json(json_attributes)
    status_code = :created
  else
    data = response_as :failure, data: resource.errors.as_pretty_json
    status_code = :unprocessable_entity
  end
  render json: data, status: status_code
end

batch_create

The batch_create action creates multiple resources in a single request. It applies authorization and returns the created resources data if successful, otherwise returns validation errors.

def batch_create
  resources = batch_new_resources
  authorize resources.first, :create?
  ActiveRecord::Base.transaction do
    if resources.all? { |resource| resource.save }
      data = response_as :success, data: resources.as_json(json_attributes)
      render json: data, status: :created
    else
      data = response_as :failure, data: resources.filter { |res| !res.errors.empty? }.map { |res| res.errors.as_pretty_json }
      render json: data, status: :unprocessable_entity
      raise ActiveRecord::Rollback
    end
  end
end

update

The update action updates an existing resource with the permitted parameters. It applies authorization and returns the updated resource data if successful, otherwise returns validation errors.

def update
  resource = requested_resource
  if resource.present?
    authorize_resource(resource)
    if resource.update(permitted_params)
      data = response_as :success, data: resource.as_json(json_attributes)
      status_code = :ok
    else
      data = response_as :failure, data: resource.errors.as_pretty_json
      status_code = :unprocessable_entity
    end
  else
    data = failure_response(:id, "Not Found")
    status_code = :not_found
  end
  render json: data, status: status_code
end

destroy

The destroy action deletes an existing resource by its ID. It applies authorization and returns the deleted resource data if successful, otherwise returns validation errors.

def destroy
  resource = requested_resource
  if resource.present?
    authorize_resource(resource)
    if resource.destroy
      data = response_as :success, data: resource.as_json(json_attributes)
      status_code = :ok
    else
      data = response_as :failure, data: resource.errors.as_pretty_json
      status_code = :unprocessable_entity
    end
  else
    data = failure_response(:id, "Not Found")
    status_code = :not_found
  end
  render json: data, status: status_code
end

batch_destroy

The batch_destroy action deletes multiple resources by their IDs. It applies authorization and deletes the resources within a transaction.

def batch_destroy
  resources = resource_class.where(id: params[:ids])
  authorize resources.first, :destroy? if !resources.blank?
  ActiveRecord::Base.transaction do
    resources.destroy_all
  end
end

batch_new_resources

Creates multiple new resources with the batch permitted parameters.

def batch_new_resources
  attribs = batch_permitted_params
  attribs.map { |attrs| resource_scope.new(attrs) }
end

requested_resource

Finds a resource by its ID.

def requested_resource
  resource_scope&.find_by_id(params[:id])
end

apply_ransack

Applies Ransack search to the resource class. Inheriting classes can override this method to supply their own ransack behavior

def apply_ransack
  resource_class.ransack(params[:q])
end

apply_order

Applies ordering to the resource scope.

def apply_order(scope)
  scope&.order(created_at: :asc)
end

apply_pagination

Applies pagination to the resource scope. By default kaminari based pagination is applied.

def apply_pagination(scope)
  scope.page(params[:page]).per(params[:per_page])
end

resource_scope

Default behavior is to return the resource class, however, inheriting classes can override this method and supply their their own scope based on the query params and type of the request.

def resource_scope
  resource_class
end

Here is an example from tags controller that includes taggings and normalizes the search query.

def resource_scope
  tags = Library::Tag.includes(:taggings).all
  tags = tags.joins(:taggings).where(taggings: {taggable_type: params[:taggable_type]}) if params[:taggable_type].present?
  tags = tags.where("NORMALIZE(name, nfkc) ilike ?", "%#{normalize_search_text(params[:query])}%") if params[:query]
  tags
end

resource_class

A mandatory method to be implemented by subclasses, this is usually the class name of the resource/model that is being dealt with.

def resource_class
  raise "to be implemented by subclass"
end

permitted_params

A mandatory method to be implemented by subclasses. This usually should contain params permitted for creating and updating the resource/model.

def permitted_params
  raise "to be implemented by subclass"
end

json_attributes

A mandatory method to be implemented by subclasses. This should contain all the fields and relationships that model wants to expose as part of the result of the successful api request.

def json_attributes
  raise "to be implemented by subclass"
end

Authorizes the search action. This is a pundit based authorization, by default authorization is delegated to index method.

def authorize_search
  authorize resource_class, :index?
end

authorize_resource

Authorizes the resource for the current action. This is a pundit based authorization. A policy file is expected to be present.

def authorize_resource(resource)
  authorize resource, "#{action_name}?".to_sym
end