-
Notifications
You must be signed in to change notification settings - Fork 532
Using JSONAPI::Resources with non ActiveRecord models and service objects
This is an example that has worked well for me in creating resources that are not explicitly backed by ActiveRecord models. Our Rails project do use ActiveRecord as our ORM, so AR is present and almost always used in conjunction with these.
Update: This recipe could (maybe should) be combined with the Singleton Resource Example
We have a Vue.js client app that connects to our Rails backend. JSON:API and JSONAPI::Resources answers many of the questions about what the API should look like. In the situation of providing aggregated data such as stats found on an admin dashboard I wanted a resource that could be called with a single GET request and provide the necessary attributes. This is a slightly contrived example as we also use PostgreSQL materialized views for many of the queries that get pulled together in objects like this.
Rails.application.routes.draw do
namespace :api do
namespace :v1 do
jsonapi_resources :dashboard, only: [:index]
end
end
end
class API::V1::DashboardController < JSONAPI::ResourceController end
class API::V1::DashboardResource < API::V1::ServicesBaseResource
model_name 'Services::Dashboard' # since these models are in a Services module this is required
key_type :uuid
paginator :none # prevents offset and limit from being called
attributes :contacts_count, :events_count
end
module Services
class Dashboard
include ActiveModel::Model
attr_reader :id, :contacts_count, :events_count
def initialize(*args)
@id = SecureRandom.uuid
end
def contacts_count
@contacts_count ||= Contact.count
end
def events_count
@events_count ||= Event.count
end
def readonly?
true
end
def persisted?
false
end
def self.all
new
end
def order(*args)
[self] # following order collect is called on the result so return an array with self
end
def count(*args)
1 # count is called for meta
end
end
end
In our use case it we needed a way to handle more complex service oriented resources. This example uses a service object MultiSessionRegistration
to create ActiveRecord registration
objects for each session
in an event
. The body of the POST request requires a contact_id
and event_id
. This takes advantage of including ActiveModel::Model
for validations. The validator pushes any duplicate errors into the errors object so that JR returns those errors appropriately.
Rails.application.routes.draw do
namespace :api do
namespace :v1 do
jsonapi_resources :multi_session_registration, only: [:create]
end
end
end
class API::V1::MultiSessionRegistrationController < JSONAPI::ResourceController end
class API::V1::MultiSessionRegistrationResource < JSONAPI::Resource
model_name 'Services::MultiSessionRegistration'
include Rails.application.routes.url_helpers
attributes :contact_id, :event_id, :package_id
def custom_links(options)
contact = Contact.find(contact_id)
base_url = options[:serializer].link_builder.base_url
{ registrations: "#{base_url}#{api_v1_contact_url(contact, only_path: true)}/registrations?filter[new]" }
end
end
require 'securerandom'
module Services
class Base
include ActiveModel::Model
attr_reader :id
class << self
def inherited(_subclass)
end
def call(args = {})
new(args).call
end
end
def call
fail NotImplementedError.new('call needs to be implemented in subclasses and return self')
end
def self.all
new
end
def order(*_args)
[self]
end
def count(*_args)
1
end
end
end
module Services
class MultiSessionRegistration < Base
attr_accessor :contact_id, :event_id, :package_id
validates_presence_of :contact
validates_presence_of :event, if: -> { package_id.blank? }
validates_presence_of :package, if: -> { event_id.blank? }
validates_with MultiSessionRegistrationValidator
def initialize(**params)
super
@errors = ActiveModel::Errors.new(self)
@package_id = params[:package_id]
@event_id = params[:event_id]
@contact_id = params[:contact_id]
end
def call
process_registrations
self
end
def save(*args)
begin
process_registrations
rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotSaved => e
Rails.logger.error(e.record.errors)
return false
end
true
end
def contact
Contact.find(@contact_id) if Contact.where(id: @contact_id).exists?
end
def event
Event.find(@event_id) if Event.where(id: @event_id).exists?
end
def package
Package.find(@package_id) if Package.where(id: @package_id).exists?
end
def self.all
raise JSONAPI::Exceptions::RecordNotFound.new(nil)
end
def self.find(id)
raise JSONAPI::Exceptions::RecordNotFound.new(nil)
end
private
def process_registrations
Registration.transaction do
registrations.each do |registration|
registration.save!
end
@id = SecureRandom.uuid
end
end
def registrations
sessions.map do |session|
create_registration_for(session)
end
end
def create_registration_for(session)
Registration.new(contact: contact, session: session)
end
def sessions
if event
return event.sessions
elsif package
return package.sessions
end
end
end
end
class MultiSessionRegistrationValidator < ActiveModel::Validator
def validate(record)
return unless record.contact_id
return unless record.event_id || record.package_id
session_ids = if record.event_id
Event.find(record.event_id).sessions.pluck(:id) if Event.where(id: record.event_id).exists?
elsif record.package_id
Package.find(record.package_id).sessions.pluck(:id) if Package.where(id: record.package_id).exists?
end
return unless session_ids
session_ids.each do |id|
if Registration.where(contact: record.contact_id, session: id).exists?
record.errors[:duplicate] << "already exists for contact: #{record.contact_id} and session #{id}."
end
end
end
end