Files
growstuff/app/resources/application_resource.rb
T

106 lines
4.4 KiB
Ruby

# frozen_string_literal: true
class ApplicationResource < Graphiti::Resource
self.abstract_class = true
self.adapter = Graphiti::Adapters::ActiveRecord
self.base_url = Rails.configuration.x.api_base_url
self.endpoint_namespace = '/api/v1'
self.default_page_size = 10
self.max_page_size = 100
# Pages need an order to be pages of the same list.
self.default_sort = [{ id: :asc }]
# The adapter pages with Kaminari, which this app doesn't use.
paginate do |scope, current_page, per_page, _context, offset|
scope.limit(per_page).offset(offset.to_i + ((current_page - 1) * per_page))
end
# Writes belong to the member holding the token, and are theirs to make. The hooks run once the attributes are
# assigned, so what is checked is what would be saved. Graphiti also runs them for records the request only links
# to, such as the crop of a planting, which are left alone.
before_save do |record|
next unless written?(record)
record.owner = context.current_user if record.new_record?
linked_records_exist!(record)
context.authorize!(record.new_record? ? :create : :update, record)
end
before_destroy do |record|
context.authorize!(:destroy, record) if written?(record)
end
# Photos are attached to plantings, gardens and harvests through the polymorphic photo_associations table.
# This is the `photos` relationship of one of those; filter[photo_id] on that resource is how photos link back.
def self.photos_of(type)
column = "#{type.underscore}_id"
has_many :photos, resource: PhotoResource, foreign_key: column,
base_scope: -> { Photo.by_model(type).select("photos.*, photo_associations.photographable_id AS #{column}") }
filter :photo_id, :integer do
eq do |scope, ids|
scope.where(id: PhotoAssociation.where(photographable_type: type, photo_id: ids).select(:photographable_id))
end
end
end
# The other side of photos_of, for PhotoResource: the plantings, gardens or harvests a photo is attached to.
def self.photographed(type)
model = type.constantize
has_many type.underscore.pluralize.to_sym, foreign_key: :photo_id,
base_scope: lambda {
model.joins(:photo_associations)
.select("#{model.table_name}.*, photo_associations.photo_id AS photo_id")
}
filter :"#{type.underscore}_id", :integer do
eq do |scope, ids|
scope.where(id: PhotoAssociation.where(photographable_type: type, photographable_id: ids).select(:photo_id))
end
end
end
# Worked out by the model, so not something the database can filter or sort by.
def self.computed_attribute(name, type, &)
attribute(name, type, writable: false, filterable: false, sortable: false, &)
end
# Records are put into a garden by someone who can change that garden, as on the website. Being a collaborator
# is not enough, and the model's own checks do not ask, so a garden that is only linked to has to be authorised.
def self.needs_writable_garden
before_save do |record|
context.authorize!(:update, record.garden) if written?(record) && record.garden && record.garden_id_changed?
end
end
# A column that can be filtered on but isn't in the response.
def self.filter_only(name, type)
attribute name, type, readable: false, writable: false
end
# Read only: it can be listed and shown, and nothing else.
def self.read_only
primary_endpoint "/#{type}", %i(index show)
end
# The member a record belongs to. It is the member holding the token that wrote it, so it is never set by a request.
def self.owned
filter_only :owner_id, :integer
belongs_to :owner, resource: MemberResource
end
private
def written?(record)
record.instance_of?(context.resource_class.model)
end
# A foreign key that points at nothing is a request for something that is not there, not a server error.
def linked_records_exist!(record)
record.class.reflect_on_all_associations(:belongs_to).reject(&:polymorphic?).each do |association|
next if record[association.foreign_key].blank? || record.association(association.name).reader
raise ActiveRecord::RecordNotFound, "Couldn't find #{association.klass.name} with id #{record[association.foreign_key]}"
end
end
end