From 1135c763de8a49d8a7916d009cfeaee4fae71eb7 Mon Sep 17 00:00:00 2001 From: Brenda Wallace Date: Sun, 25 Oct 2026 11:48:28 +1300 Subject: [PATCH] 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. --- app/controllers/api/docs_controller.rb | 1 + app/controllers/api/v1/base_controller.rb | 9 ++---- app/resources/application_resource.rb | 39 +++++++++++++++-------- app/resources/crop_resource.rb | 14 ++++---- app/resources/garden_resource.rb | 3 +- app/resources/harvest_resource.rb | 3 +- app/resources/job_resource.rb | 3 +- app/resources/member_resource.rb | 6 ++-- app/resources/photo_resource.rb | 15 ++++----- app/resources/planting_resource.rb | 3 +- app/resources/seed_resource.rb | 3 +- config/environments/production.rb | 2 +- 12 files changed, 52 insertions(+), 49 deletions(-) diff --git a/app/controllers/api/docs_controller.rb b/app/controllers/api/docs_controller.rb index 5dc76c0bb..675177632 100644 --- a/app/controllers/api/docs_controller.rb +++ b/app/controllers/api/docs_controller.rb @@ -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 diff --git a/app/controllers/api/v1/base_controller.rb b/app/controllers/api/v1/base_controller.rb index a6fd2570d..37e3950e9 100644 --- a/app/controllers/api/v1/base_controller.rb +++ b/app/controllers/api/v1/base_controller.rb @@ -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 diff --git a/app/resources/application_resource.rb b/app/resources/application_resource.rb index 42a9d2b1d..62a0bf585 100644 --- a/app/resources/application_resource.rb +++ b/app/resources/application_resource.rb @@ -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 diff --git a/app/resources/crop_resource.rb b/app/resources/crop_resource.rb index 1af61678f..b6097777b 100644 --- a/app/resources/crop_resource.rb +++ b/app/resources/crop_resource.rb @@ -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| diff --git a/app/resources/garden_resource.rb b/app/resources/garden_resource.rb index 402427122..781ff15af 100644 --- a/app/resources/garden_resource.rb +++ b/app/resources/garden_resource.rb @@ -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 diff --git a/app/resources/harvest_resource.rb b/app/resources/harvest_resource.rb index b09d8abfa..f3fc46843 100644 --- a/app/resources/harvest_resource.rb +++ b/app/resources/harvest_resource.rb @@ -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| diff --git a/app/resources/job_resource.rb b/app/resources/job_resource.rb index eb7737e3d..77b04246e 100644 --- a/app/resources/job_resource.rb +++ b/app/resources/job_resource.rb @@ -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 diff --git a/app/resources/member_resource.rb b/app/resources/member_resource.rb index 97cf87dba..cc4e507f0 100644 --- a/app/resources/member_resource.rb +++ b/app/resources/member_resource.rb @@ -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 diff --git a/app/resources/photo_resource.rb b/app/resources/photo_resource.rb index 005389d9e..e8bb4ef28 100644 --- a/app/resources/photo_resource.rb +++ b/app/resources/photo_resource.rb @@ -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 diff --git a/app/resources/planting_resource.rb b/app/resources/planting_resource.rb index 9aa68cc19..f63fe9db6 100644 --- a/app/resources/planting_resource.rb +++ b/app/resources/planting_resource.rb @@ -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 diff --git a/app/resources/seed_resource.rb b/app/resources/seed_resource.rb index 79bcfcdbd..71af7d129 100644 --- a/app/resources/seed_resource.rb +++ b/app/resources/seed_resource.rb @@ -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 diff --git a/config/environments/production.rb b/config/environments/production.rb index 55ccabc37..77547b3ef 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -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