Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions app/serializers/alchemy/json_api/element_serializer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,13 @@ class ElementSerializer < BaseSerializer
end

has_many :ingredients,
lazy_load_data: true,
serializer: ->(record) do
"Alchemy::JsonApi::Ingredient#{record.type.demodulize}Serializer".constantize
end

# Eager: the element tree (all_elements -> nested_elements) is recursive
# and can't be expressed as an `include`, so its linkage is always present.
has_many :nested_elements, record_type: :element, serializer: self
end
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ class IngredientNodeSerializer < BaseSerializer
ingredient.node&.name
end

belongs_to :node, record_type: :node, serializer: ::Alchemy::JsonApi::NodeSerializer
belongs_to :node, record_type: :node, serializer: ::Alchemy::JsonApi::NodeSerializer, lazy_load_data: true

with_options if: proc { |ingredient| ingredient.node } do
attribute :name do |ingredient|
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ class IngredientPageSerializer < BaseSerializer
ingredient.page&.url_path
end

has_one :page, record_type: :page, serializer: PageSerializer do |ingredient|
has_one :page, record_type: :page, serializer: PageSerializer, lazy_load_data: true do |ingredient|
Alchemy::JsonApi::Page.new(ingredient.page) if ingredient.page
end
end
Expand Down
8 changes: 4 additions & 4 deletions app/serializers/alchemy/json_api/language_serializer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,13 @@ class LanguageSerializer < BaseSerializer
:locale
)

has_many :menu_items, record_type: :node, serializer: ::Alchemy::JsonApi::NodeSerializer, object_method_name: :nodes, id_method_name: :node_ids
has_many :menu_items, record_type: :node, serializer: ::Alchemy::JsonApi::NodeSerializer, object_method_name: :nodes, id_method_name: :node_ids, lazy_load_data: true

has_many :menus, record_type: :node, serializer: ::Alchemy::JsonApi::NodeSerializer do |language|
has_many :menus, record_type: :node, serializer: ::Alchemy::JsonApi::NodeSerializer, lazy_load_data: true do |language|
language.nodes.select { |n| n.parent.nil? }
end
has_many :pages
has_one :root_page, record_type: :page, serializer: ::Alchemy::JsonApi::PageSerializer
has_many :pages, lazy_load_data: true
has_one :root_page, record_type: :page, serializer: ::Alchemy::JsonApi::PageSerializer, lazy_load_data: true
end
end
end
7 changes: 4 additions & 3 deletions app/serializers/alchemy/json_api/node_serializer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,19 @@ class NodeSerializer < BaseSerializer
attribute :link_title, &:title
attribute :link_nofollow, &:nofollow

belongs_to :parent, record_type: :node, serializer: self
belongs_to :parent, record_type: :node, serializer: self, lazy_load_data: true

belongs_to(
:page,
record_type: :page,
if: ->(node) { node.page },
serializer: ::Alchemy::JsonApi::PageSerializer
serializer: ::Alchemy::JsonApi::PageSerializer,
lazy_load_data: true
) do |node|
::Alchemy::JsonApi::Page.new(node.page)
end

has_many :children, record_type: :node, serializer: self
has_many :children, record_type: :node, serializer: self, lazy_load_data: true
end
end
end
14 changes: 9 additions & 5 deletions app/serializers/alchemy/json_api/page_serializer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -33,24 +33,28 @@ class PageSerializer < BaseSerializer
end
end

belongs_to :language, record_type: :language, serializer: ::Alchemy::JsonApi::LanguageSerializer
belongs_to :language, record_type: :language, serializer: ::Alchemy::JsonApi::LanguageSerializer, lazy_load_data: true

has_many :ancestors, record_type: :page, serializer: self do |page|
has_many :ancestors, record_type: :page, serializer: self, lazy_load_data: true do |page|
page.ancestors.map do |ancestor|
Alchemy::JsonApi::Page.new(ancestor, page_version_type: page.page_version_type)
end
end

# All public elements of this page regardless of if they are fixed or nested.
# Used for eager loading and should be used as the +include+ parameter of your query
# Used for eager loading and should be used as the +include+ parameter of your query.
# Eager: paired with the recursive nested_elements linkage to build the element
# tree, which can't be expressed as an `include`.
has_many :all_elements, record_type: :element, serializer: ELEMENT_SERIALIZER

# The top level public, non-fixed elements of this page that - if present -
# contains their nested_elements.
# contains their nested_elements. Eager: the roots of the element tree
# (see all_elements above).
has_many :elements, record_type: :element, serializer: ELEMENT_SERIALIZER

# The top level public, fixed elements of this page that - if present -
# contains their nested_elements.
# contains their nested_elements. Eager: the roots of the element tree
# (see all_elements above).
has_many :fixed_elements, record_type: :element, serializer: ELEMENT_SERIALIZER
end
end
Expand Down
3 changes: 3 additions & 0 deletions lib/alchemy/json_api/engine.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
# frozen_string_literal: true

require "jsonapi"
# Fixes jsonapi-serializer's `lazy_load_data` for nested includes. Loaded at
# boot (rather than via an autoloader) because it patches a gem constant.
require "alchemy/json_api/patches/fast_jsonapi/serialization_core_patch"

module Alchemy
module JsonApi
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# frozen_string_literal: true

require "jsonapi/serializer"

module Alchemy
module JsonApi
module Patches
module FastJsonapi
# Makes jsonapi-serializer's `lazy_load_data` honour *nested* includes.
#
# With `lazy_load_data: true` a relationship only emits its linkage when
# it was requested, which upstream (jsonapi-serializer 2.2.0) decides
# with `includes_list.include?(key)`. Two things break that for any
# sideloaded (nested) resource:
#
# 1. the includes are dotted paths (`primary_taxon.ancestors`), so
# `include?(:primary_taxon)` never matches; and
# 2. `get_included_records` hands each sideloaded record the *parent's*
# includes_list instead of the scoped remainder, so a nested
# resource judges its own relationships against the wrong paths.
#
# Together these suppress linkage for *every* relationship of a nested
# resource -- even requested ones -- making lazy_load_data unusable
# beyond top-level relationships.
#
# The two overrides below fix each half: `relationships_hash` matches the
# base key of every requested path, and `get_included_records` threads
# the scoped remainder into each sideloaded record. Relationships that do
# not set `lazy_load_data` are unaffected (their linkage always emits).
# Prepended onto the shared SerializationCore, so every serializer gets it.
module SerializationCorePatch
# Upstream tests `includes_list.include?(key)` verbatim. Normalising the
# list to the set of base keys first makes that check match dotted paths,
# so the rest of upstream's implementation can stand.
def relationships_hash(record, relationships = nil, fieldset = nil, includes_list = nil, params = {})
super(record, relationships, fieldset, base_include_keys(includes_list), params)
end

# Verbatim copy of FastJsonapi::SerializationCore 2.2.0 with a single
# change, flagged inline below -- keep it diffable against upstream.
def get_included_records(record, includes_list, known_included_objects, fieldsets, params = {})
return unless includes_list.present?
return [] unless relationships_to_serialize

includes_list = parse_includes_list(includes_list)

includes_list.each_with_object([]) do |include_item, included_records|
relationship_item = relationships_to_serialize[include_item.first]

next unless relationship_item&.include_relationship?(record, params)

included_objects = Array(relationship_item.fetch_associated_object(record, params))
next if included_objects.empty?

static_serializer = relationship_item.static_serializer
static_record_type = relationship_item.static_record_type

included_objects.each do |inc_obj|
serializer = static_serializer || relationship_item.serializer_for(inc_obj, params)
record_type = static_record_type || serializer.record_type

if include_item.last.any?
serializer_records = serializer.get_included_records(inc_obj, include_item.last, known_included_objects, fieldsets, params)
included_records.concat(serializer_records) unless serializer_records.empty?
end

code = "#{record_type}_#{serializer.id_from_record(inc_obj, params)}"
next if known_included_objects.include?(code)

known_included_objects << code

# CHANGED vs upstream: pass this include's remainder rather than
# the parent's parsed includes_list, so the sideloaded record
# scopes its own relationships correctly.
included_records << serializer.record_hash(inc_obj, fieldsets[record_type], include_item.last, params)
end
end
end

private

# The base relationship keys requested at this level, e.g.
# `[:primary_taxon.ancestors, :author]` -> Set[:primary_taxon, :author].
def base_include_keys(includes_list)
return Set.new if includes_list.blank?

includes_list.map { |path| path.to_s.split(".", 2).first.to_sym }.to_set
end

::FastJsonapi::SerializationCore::ClassMethods.prepend(self)
end
end
end
end
end
2 changes: 1 addition & 1 deletion spec/requests/alchemy/json_api/admin/layout_pages_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@

it "loads elements from draft version" do
element = FactoryBot.create(:alchemy_element, page_version: page.draft_version)
subject
get alchemy_json_api.admin_layout_page_path(page)
document = JSON.parse(response.body)
expect(document["data"]["relationships"]["elements"]).to eq(
{
Expand Down
2 changes: 1 addition & 1 deletion spec/requests/alchemy/json_api/admin/pages_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@

it "loads elements from draft version" do
element = FactoryBot.create(:alchemy_element, page_version: page.draft_version)
subject
get alchemy_json_api.admin_page_path(page)
document = JSON.parse(response.body)
expect(document["data"]["relationships"]["elements"]).to eq(
{
Expand Down
4 changes: 3 additions & 1 deletion spec/serializers/alchemy/json_api/element_serializer_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,10 @@
end

context "with ingredients" do
let(:options) { {include: [:ingredients]} }

before do
expect(element).to receive(:ingredients) do
allow(element).to receive(:ingredients) do
[
FactoryBot.build_stubbed(:alchemy_ingredient_text, element: element),
FactoryBot.build_stubbed(:alchemy_ingredient_richtext, element: element),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
end

describe "relationships" do
subject { serializer.serializable_hash[:data][:relationships] }
subject { described_class.new(ingredient, include: [:node]).serializable_hash[:data][:relationships] }

it "has the right keys and values" do
expect(subject[:node]).to eq(data: {id: node.id.to_s, type: :node})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
end

describe "relationships" do
subject { serializer.serializable_hash[:data][:relationships] }
subject { described_class.new(ingredient, include: [:page]).serializable_hash[:data][:relationships] }

it "has page object" do
expect(subject[:page]).to eq(data: {id: page.id.to_s, type: :page})
Expand Down
9 changes: 6 additions & 3 deletions spec/serializers/alchemy/json_api/language_serializer_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,16 @@
let!(:menu) { FactoryBot.create(:alchemy_node, language: language) }
let!(:menu_node) { FactoryBot.create(:alchemy_node, language: language, parent: menu) }

let(:options) { {include: [:menus, :menu_items]} }

subject { serializer.serializable_hash[:data][:relationships] }

it "has the right keys and values" do
expect(subject[:root_page]).to eq(data: {id: root_page.id.to_s, type: :page})
expect(subject[:pages]).to eq(data: [{id: root_page.id.to_s, type: :page}])
it "exposes requested relationships and omits the rest" do
expect(subject[:menus]).to eq(data: [{id: menu.id.to_s, type: :node}])
expect(subject[:menu_items]).to eq(data: [{id: menu.id.to_s, type: :node}, {id: menu_node.id.to_s, type: :node}])
# root_page and pages were not requested, so their linkage is omitted
expect(subject[:root_page]).to eq({})
expect(subject[:pages]).to eq({})
end
end
end
Loading
Loading