Tidy API resources: read_only and owned macros, private hook helpers

Declare read-only resources and the owner relationship once each, make
the resource hook helpers private, simplify token auth, and cache the
generated API docs for an hour.
This commit is contained in:
Brenda Wallace committed 2026-10-25 11:48:28 +13:00
1 parent 086140205d
commit 1135c763de
12 files changed
+52 -49

No files matched your search

+1
View File
@@ -4,6 +4,7 @@ module Api
# The API's own description, for Swagger UI at /api-docs.
class DocsController < ActionController::API
def show
expires_in 1.hour, public: true
render json: ApiDocs.openapi
end
end
+2 -7
View File
@@ -39,7 +39,7 @@ module Api
def update
resource = resource_class.find(params)
# As it is now; the resource hook then checks what it would become.
# The record as it is; the resource then checks what the changes would make of it.
authorize! :update, resource.data
render_write resource, resource.update_attributes
end
@@ -89,12 +89,7 @@ module Api
def authenticate_member_from_token!
authenticate_with_http_token do |token, _options|
auth = Authentication.find_by(token: token, provider: 'api')
if auth.present?
@current_user = auth.member
return true
end
@current_user = Authentication.find_by(token: token, provider: 'api')&.member
end
end
end
+26 -13
View File
@@ -31,19 +31,6 @@ class ApplicationResource < Graphiti::Resource
context.authorize!(:destroy, record) if written?(record)
end
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
# 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)
@@ -81,4 +68,30 @@ class ApplicationResource < Graphiti::Resource
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
+7 -7
View File
@@ -2,15 +2,15 @@
class CropResource < ApplicationResource
self.model = Crop
primary_endpoint '/crops', %i(index show)
read_only
secondary_endpoint '/crops/search', [:search]
attribute :name, :string, writable: false
attribute :en_wikipedia_url, :string, writable: false
attribute :perennial, :boolean, writable: false
attribute :median_lifespan, :integer, writable: false
attribute :median_days_to_first_harvest, :integer, writable: false
attribute :median_days_to_last_harvest, :integer, writable: false
attribute :name, :string
attribute :en_wikipedia_url, :string
attribute :perennial, :boolean
attribute :median_lifespan, :integer
attribute :median_days_to_first_harvest, :integer
attribute :median_days_to_last_harvest, :integer
filter_only :approval_status, :string
default_filter :approval_status do |scope|
+1 -2
View File
@@ -7,10 +7,9 @@ class GardenResource < ApplicationResource
filter_only :slug, :string
filter_only :active, :boolean
filter_only :location, :string
filter_only :owner_id, :integer
filter_only :garden_type_id, :integer
belongs_to :owner, resource: MemberResource
owned
has_many :plantings
photos_of 'Garden'
end
+1 -2
View File
@@ -12,12 +12,11 @@ class HarvestResource < ApplicationResource
attribute :crop_id, :integer, readable: false
attribute :planting_id, :integer, readable: false
filter_only :owner_id, :integer
filter_only :plant_part_id, :integer
belongs_to :crop
belongs_to :planting
belongs_to :owner, resource: MemberResource
owned
photos_of 'Harvest'
before_save do |harvest|
+1 -2
View File
@@ -12,8 +12,7 @@ class JobResource < ApplicationResource
attribute :action, :string
attribute :garden_id, :integer, readable: false
filter_only :owner_id, :integer
belongs_to :garden
belongs_to :owner, resource: MemberResource
owned
end
+3 -3
View File
@@ -2,10 +2,10 @@
class MemberResource < ApplicationResource
self.model = Member
primary_endpoint '/members', %i(index show)
read_only
attribute :login_name, :string, writable: false
attribute :slug, :string, writable: false
attribute :login_name, :string
attribute :slug, :string
has_many :gardens, foreign_key: :owner_id
has_many :plantings, foreign_key: :owner_id
+7 -8
View File
@@ -2,19 +2,18 @@
class PhotoResource < ApplicationResource
self.model = Photo
primary_endpoint '/photos', %i(index show)
read_only
attribute :thumbnail_url, :string, writable: false
attribute :fullsize_url, :string, writable: false
attribute :license_name, :string, writable: false
attribute :link_url, :string, writable: false
attribute :title, :string, writable: false
filter_only :owner_id, :integer
attribute :thumbnail_url, :string
attribute :fullsize_url, :string
attribute :license_name, :string
attribute :link_url, :string
attribute :title, :string
filter :crop_id, :integer do
eq { |scope, ids| scope.where(id: PhotoAssociation.where(crop_id: ids).select(:photo_id)) }
end
belongs_to :owner, resource: MemberResource
owned
%w(Planting Garden Harvest).each { |type| photographed type }
end
+1 -2
View File
@@ -31,7 +31,6 @@ class PlantingResource < ApplicationResource
attribute :crop_id, :integer, readable: false
attribute :garden_id, :integer, readable: false
filter_only :owner_id, :integer
filter :active, :boolean do
eq { |scope, value| value ? scope.active : scope }
@@ -42,7 +41,7 @@ class PlantingResource < ApplicationResource
belongs_to :garden
belongs_to :crop
belongs_to :owner, resource: MemberResource
owned
has_many :harvests
photos_of 'Planting'
end
+1 -2
View File
@@ -14,8 +14,7 @@ class SeedResource < ApplicationResource
attribute :heirloom, :string
attribute :crop_id, :integer, readable: false
filter_only :owner_id, :integer
belongs_to :crop
belongs_to :owner, resource: MemberResource
owned
end
+1 -1
View File
@@ -98,7 +98,7 @@ Rails.application.configure do
ActionMailer::Base.delivery_method = :smtp
config.host = ENV.fetch('HOST', nil)
config.x.api_base_url = "https://#{config.host}" if config.host.present? && ENV['API_BASE_URL'].blank?
config.x.api_base_url = ENV.fetch('API_BASE_URL') { "https://#{config.host}" }
config.analytics_code = <<-GET_CLICKY_SCRIPT
<script src="//static.getclicky.com/js" type="text/javascript"></script>
<script type="text/javascript">try{ clicky.init(100594260); }catch(e){}</script>