diff --git a/app/controllers/alchemy/json_api/pages_controller.rb b/app/controllers/alchemy/json_api/pages_controller.rb index bc1aee1..817ed47 100644 --- a/app/controllers/alchemy/json_api/pages_controller.rb +++ b/app/controllers/alchemy/json_api/pages_controller.rb @@ -38,10 +38,10 @@ def show def render_pages_json(allowed) # Only load pages with all includes when browser cache is stale jsonapi_filter(page_scope_with_includes, allowed) do |filtered| - # decorate with our page model that has a eager loaded elements collection - filtered_pages = filtered.result.map { |page| api_page(page) } - jsonapi_paginate(filtered_pages) do |paginated| - render jsonapi: paginated + jsonapi_paginate(filtered.result) do |paginated| + # decorate with our page model that has a eager loaded elements collection + decorated_pages = preload_ingredients(paginated).map { |page| api_page(page) } + render jsonapi: decorated_pages end end end @@ -74,7 +74,9 @@ def jsonapi_meta(pages) end def load_page - @page = load_page_by_id || load_page_by_urlname || raise(ActiveRecord::RecordNotFound) + @page = preload_ingredients( + [load_page_by_id || load_page_by_urlname || raise(ActiveRecord::RecordNotFound)] + ).first end def load_page_by_id @@ -109,6 +111,14 @@ def page_scope_with_includes ) end + def preload_ingredients(scope) + if params[:include]&.match?(/ingredients/) + Alchemy::JsonApi::Page.preload_ingredient_relations(scope, page_version_type) + else + scope + end + end + def page_version_type :public_version end @@ -129,6 +139,42 @@ def base_page_scope def jsonapi_serializer_class(_resource, _is_collection) ::Alchemy::JsonApi::PageSerializer end + + # These overrides have to be in place until + # https://github.com/stas/jsonapi.rb/pull/91 + # is merged and released + def jsonapi_paginate(resources) + @_jsonapi_original_size = resources.size + super + end + + def jsonapi_pagination_meta(resources) + return {} unless JSONAPI::Rails.is_collection?(resources) + + _, limit, page = jsonapi_pagination_params + + numbers = { current: page } + + total = @_jsonapi_original_size + + last_page = [1, (total.to_f / limit).ceil].max + + if page > 1 + numbers[:first] = 1 + numbers[:prev] = page - 1 + end + + if page < last_page + numbers[:next] = page + 1 + numbers[:last] = last_page + end + + if total.present? + numbers[:records] = total + end + + numbers + end end end end diff --git a/app/models/alchemy/json_api/page.rb b/app/models/alchemy/json_api/page.rb index 4e68668..42003c2 100644 --- a/app/models/alchemy/json_api/page.rb +++ b/app/models/alchemy/json_api/page.rb @@ -7,6 +7,23 @@ def Page.ransackable_attributes(_auth_object = nil) module JsonApi class Page < SimpleDelegator + def self.preload_ingredient_relations(pages, page_version_type) + pages.map { |page| page.send(page_version_type) }.flat_map(&:elements).flat_map(&:ingredients).group_by do |ingredient| + "Alchemy::JsonApi::Ingredient#{ingredient.type.demodulize}Serializer".constantize.preload_relations + end.each do |preload_relations, ingredients| + preload(records: ingredients.map(&:related_object).compact, associations: preload_relations) + end + pages + end + + def self.preload(records:, associations:) + if Rails::VERSION::MAJOR >= 7 + ActiveRecord::Associations::Preloader.new(records: records, associations: associations).call + else + ActiveRecord::Associations::Preloader.new.preload(records, associations) + end + end + attr_reader :page_version_type, :page_version def initialize(page, page_version_type: :public_version) diff --git a/app/serializers/alchemy/json_api/base_serializer.rb b/app/serializers/alchemy/json_api/base_serializer.rb index 18cae81..e227fea 100644 --- a/app/serializers/alchemy/json_api/base_serializer.rb +++ b/app/serializers/alchemy/json_api/base_serializer.rb @@ -4,6 +4,12 @@ class BaseSerializer include JSONAPI::Serializer set_key_transform Alchemy::JsonApi.key_transform + + # This method can be overwritten in individual serializers to fetch objects that belong to the related object in some form. + # This takes an Array of relation names that can be passed to the Rails preloader. + def self.preload_relations + [] + end end end end diff --git a/app/serializers/alchemy/json_api/ingredient_picture_serializer.rb b/app/serializers/alchemy/json_api/ingredient_picture_serializer.rb index 88fea00..343d078 100644 --- a/app/serializers/alchemy/json_api/ingredient_picture_serializer.rb +++ b/app/serializers/alchemy/json_api/ingredient_picture_serializer.rb @@ -7,6 +7,10 @@ module JsonApi class IngredientPictureSerializer < BaseSerializer include IngredientSerializer + def self.preload_relations + [:thumbs] + end + attributes( :title, :caption, diff --git a/spec/serializers/alchemy/json_api/base_serializer_spec.rb b/spec/serializers/alchemy/json_api/base_serializer_spec.rb index e45890d..42bbc3a 100644 --- a/spec/serializers/alchemy/json_api/base_serializer_spec.rb +++ b/spec/serializers/alchemy/json_api/base_serializer_spec.rb @@ -9,4 +9,10 @@ expect(described_class).to receive(:set_key_transform).with(:underscore) load Alchemy::JsonApi::Engine.root.join("app/serializers/alchemy/json_api/base_serializer.rb") end + + describe ".preload_relations" do + subject { described_class.preload_relations } + + it { is_expected.to eq([]) } + end end diff --git a/spec/serializers/alchemy/json_api/ingredient_picture_serializer_spec.rb b/spec/serializers/alchemy/json_api/ingredient_picture_serializer_spec.rb index 70c8c23..a3d306b 100644 --- a/spec/serializers/alchemy/json_api/ingredient_picture_serializer_spec.rb +++ b/spec/serializers/alchemy/json_api/ingredient_picture_serializer_spec.rb @@ -18,6 +18,12 @@ it_behaves_like "an ingredient serializer" + describe ".preload_relations" do + subject { described_class.preload_relations } + + it { is_expected.to eq([:thumbs]) } + end + describe "attributes" do subject { serializer.serializable_hash[:data][:attributes] }