From d7986e04fa49ae93a209b3a8e183b808c7937eac Mon Sep 17 00:00:00 2001 From: Thomas von Deyen Date: Tue, 1 Sep 2026 07:59:20 +0200 Subject: [PATCH] feat(serializer): lazy-load relationship data by default Emitting relationship linkage that the request's `include` never asked for is surprising to clients and triggers N+1 queries on the server, so every relationship now sets `lazy_load_data: true` and emits its `data` only when requested. The element tree is the exception and stays eager (`all_elements`, `elements`, `fixed_elements`, `nested_elements`): a tree of arbitrary depth cannot be expressed as an `include` path, and clients rely on the top-level linkage as the entry points to reconstruct the page's content. This requires making jsonapi-serializer's `lazy_load_data` honour nested includes: upstream matches `includes_list.include?(key)`, but top-level includes are dotted paths and each sideloaded record receives the parent's includes_list, so it suppressed linkage for every relationship of a nested resource. A SerializationCore patch, prepended at boot, matches the base key of each requested path and threads the scoped remainder into each sideloaded record. --- .../alchemy/json_api/element_serializer.rb | 3 + .../json_api/ingredient_node_serializer.rb | 2 +- .../json_api/ingredient_page_serializer.rb | 2 +- .../alchemy/json_api/language_serializer.rb | 8 +- .../alchemy/json_api/node_serializer.rb | 7 +- .../alchemy/json_api/page_serializer.rb | 14 +- lib/alchemy/json_api/engine.rb | 3 + .../fast_jsonapi/serialization_core_patch.rb | 95 ++++++++ .../json_api/admin/layout_pages_spec.rb | 2 +- .../alchemy/json_api/admin/pages_spec.rb | 2 +- .../json_api/element_serializer_spec.rb | 4 +- .../ingredient_node_serializer_spec.rb | 2 +- .../ingredient_page_serializer_spec.rb | 2 +- .../json_api/language_serializer_spec.rb | 9 +- .../json_api/lazy_load_data_nested_spec.rb | 209 ++++++++++++++++++ .../alchemy/json_api/node_serializer_spec.rb | 4 + .../alchemy/json_api/page_serializer_spec.rb | 6 + 17 files changed, 352 insertions(+), 22 deletions(-) create mode 100644 lib/alchemy/json_api/patches/fast_jsonapi/serialization_core_patch.rb create mode 100644 spec/serializers/alchemy/json_api/lazy_load_data_nested_spec.rb diff --git a/app/serializers/alchemy/json_api/element_serializer.rb b/app/serializers/alchemy/json_api/element_serializer.rb index d6b10c86..03e528a8 100644 --- a/app/serializers/alchemy/json_api/element_serializer.rb +++ b/app/serializers/alchemy/json_api/element_serializer.rb @@ -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 diff --git a/app/serializers/alchemy/json_api/ingredient_node_serializer.rb b/app/serializers/alchemy/json_api/ingredient_node_serializer.rb index 95630c1b..7059ba29 100644 --- a/app/serializers/alchemy/json_api/ingredient_node_serializer.rb +++ b/app/serializers/alchemy/json_api/ingredient_node_serializer.rb @@ -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| diff --git a/app/serializers/alchemy/json_api/ingredient_page_serializer.rb b/app/serializers/alchemy/json_api/ingredient_page_serializer.rb index 556291bf..ccb28a07 100644 --- a/app/serializers/alchemy/json_api/ingredient_page_serializer.rb +++ b/app/serializers/alchemy/json_api/ingredient_page_serializer.rb @@ -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 diff --git a/app/serializers/alchemy/json_api/language_serializer.rb b/app/serializers/alchemy/json_api/language_serializer.rb index f071519a..a3514fed 100644 --- a/app/serializers/alchemy/json_api/language_serializer.rb +++ b/app/serializers/alchemy/json_api/language_serializer.rb @@ -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 diff --git a/app/serializers/alchemy/json_api/node_serializer.rb b/app/serializers/alchemy/json_api/node_serializer.rb index afbc1daf..4baf7cd4 100644 --- a/app/serializers/alchemy/json_api/node_serializer.rb +++ b/app/serializers/alchemy/json_api/node_serializer.rb @@ -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 diff --git a/app/serializers/alchemy/json_api/page_serializer.rb b/app/serializers/alchemy/json_api/page_serializer.rb index 260e2956..985a0f18 100644 --- a/app/serializers/alchemy/json_api/page_serializer.rb +++ b/app/serializers/alchemy/json_api/page_serializer.rb @@ -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 diff --git a/lib/alchemy/json_api/engine.rb b/lib/alchemy/json_api/engine.rb index 7b0bc1f0..fab2b671 100644 --- a/lib/alchemy/json_api/engine.rb +++ b/lib/alchemy/json_api/engine.rb @@ -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 diff --git a/lib/alchemy/json_api/patches/fast_jsonapi/serialization_core_patch.rb b/lib/alchemy/json_api/patches/fast_jsonapi/serialization_core_patch.rb new file mode 100644 index 00000000..78e11012 --- /dev/null +++ b/lib/alchemy/json_api/patches/fast_jsonapi/serialization_core_patch.rb @@ -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 diff --git a/spec/requests/alchemy/json_api/admin/layout_pages_spec.rb b/spec/requests/alchemy/json_api/admin/layout_pages_spec.rb index 23f515a7..686a48fc 100644 --- a/spec/requests/alchemy/json_api/admin/layout_pages_spec.rb +++ b/spec/requests/alchemy/json_api/admin/layout_pages_spec.rb @@ -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( { diff --git a/spec/requests/alchemy/json_api/admin/pages_spec.rb b/spec/requests/alchemy/json_api/admin/pages_spec.rb index d34ed8ab..bba2199b 100644 --- a/spec/requests/alchemy/json_api/admin/pages_spec.rb +++ b/spec/requests/alchemy/json_api/admin/pages_spec.rb @@ -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( { diff --git a/spec/serializers/alchemy/json_api/element_serializer_spec.rb b/spec/serializers/alchemy/json_api/element_serializer_spec.rb index 79462c58..03de583e 100644 --- a/spec/serializers/alchemy/json_api/element_serializer_spec.rb +++ b/spec/serializers/alchemy/json_api/element_serializer_spec.rb @@ -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), diff --git a/spec/serializers/alchemy/json_api/ingredient_node_serializer_spec.rb b/spec/serializers/alchemy/json_api/ingredient_node_serializer_spec.rb index b891ae42..87756ff8 100644 --- a/spec/serializers/alchemy/json_api/ingredient_node_serializer_spec.rb +++ b/spec/serializers/alchemy/json_api/ingredient_node_serializer_spec.rb @@ -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}) diff --git a/spec/serializers/alchemy/json_api/ingredient_page_serializer_spec.rb b/spec/serializers/alchemy/json_api/ingredient_page_serializer_spec.rb index 996aee7a..5a7a47d7 100644 --- a/spec/serializers/alchemy/json_api/ingredient_page_serializer_spec.rb +++ b/spec/serializers/alchemy/json_api/ingredient_page_serializer_spec.rb @@ -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}) diff --git a/spec/serializers/alchemy/json_api/language_serializer_spec.rb b/spec/serializers/alchemy/json_api/language_serializer_spec.rb index c51f072c..24a3553f 100644 --- a/spec/serializers/alchemy/json_api/language_serializer_spec.rb +++ b/spec/serializers/alchemy/json_api/language_serializer_spec.rb @@ -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 diff --git a/spec/serializers/alchemy/json_api/lazy_load_data_nested_spec.rb b/spec/serializers/alchemy/json_api/lazy_load_data_nested_spec.rb new file mode 100644 index 00000000..45715608 --- /dev/null +++ b/spec/serializers/alchemy/json_api/lazy_load_data_nested_spec.rb @@ -0,0 +1,209 @@ +# frozen_string_literal: true + +require "rails_helper" + +# Covers the FastJsonapi::SerializationCore patch that makes `lazy_load_data` +# honour nested includes (loaded from the engine). Because the patch vendors a +# verbatim copy of upstream's `get_included_records`, these specs also pin the +# copied behaviour we now own: sideloading, deduplication, deep nesting and +# has_many relationships. +module LazyLoadDataNestedSpec + class Record + def initialize(**attributes) = @attributes = attributes + def method_missing(name, *) = @attributes.key?(name) ? @attributes[name] : super + def respond_to_missing?(name, *) = @attributes.key?(name) || super + end + + class LeafSerializer + include JSONAPI::Serializer + + set_type :leaf + attribute(:name) { |record| record.name } + end + + class ParentSerializer + include JSONAPI::Serializer + + set_type :parent + belongs_to(:wanted, serializer: LeafSerializer, lazy_load_data: true) { |r| r.wanted } + belongs_to(:unwanted, serializer: LeafSerializer, lazy_load_data: true) { |r| r.unwanted } + end + + class RootSerializer + include JSONAPI::Serializer + + set_type :root + belongs_to(:parent, serializer: ParentSerializer, lazy_load_data: true) { |r| r.parent } + end + + # Relationship without lazy_load_data -- must be unaffected by the patch. + class EagerSerializer + include JSONAPI::Serializer + + set_type :eager + belongs_to(:leaf, serializer: LeafSerializer) { |r| r.leaf } + end + + # Self-referential, like Alchemy's nested_elements, to exercise depth >= 3. + class TreeSerializer + include JSONAPI::Serializer + + set_type :tree + attribute(:name) { |r| r.name } + belongs_to(:child, serializer: TreeSerializer, lazy_load_data: true) { |r| r.child } + end + + # Two relationships pointing at the same record, to exercise deduplication. + class SharedRefSerializer + include JSONAPI::Serializer + + set_type :shared_ref + belongs_to(:first, serializer: LeafSerializer, lazy_load_data: true) { |r| r.shared } + belongs_to(:second, serializer: LeafSerializer, lazy_load_data: true) { |r| r.shared } + end + + class CollectionSerializer + include JSONAPI::Serializer + + set_type :collection + has_many(:leaves, serializer: LeafSerializer, lazy_load_data: true) { |r| r.leaves } + has_many(:others, serializer: LeafSerializer, lazy_load_data: true) { |r| r.others } + end + + # Nested has_many, mirroring the real all_elements.ingredients shape. + class CollectionRootSerializer + include JSONAPI::Serializer + + set_type :collection_root + belongs_to(:child, serializer: CollectionSerializer, lazy_load_data: true) { |r| r.child } + end +end + +RSpec.describe "FastJsonapi lazy_load_data patch (nested includes)" do + let(:root) do + LazyLoadDataNestedSpec::Record.new( + id: "1", + parent: LazyLoadDataNestedSpec::Record.new( + id: "10", + wanted: LazyLoadDataNestedSpec::Record.new(id: "100", name: "wanted"), + unwanted: LazyLoadDataNestedSpec::Record.new(id: "200", name: "unwanted") + ) + ) + end + + def relationship(hash, type, id, name) + node = ([hash[:data]] + Array(hash[:included])).find { |r| r && r[:type] == type && r[:id] == id } + node&.dig(:relationships, name) + end + + def included_of(hash, type) + Array(hash[:included]).select { |r| r[:type] == type } + end + + it "emits data for a requested nested relationship" do + hash = LazyLoadDataNestedSpec::RootSerializer.new(root, include: ["parent.wanted"]).serializable_hash + expect(relationship(hash, :parent, "10", :wanted)).to have_key(:data) + end + + it "omits data for an unrequested nested relationship" do + hash = LazyLoadDataNestedSpec::RootSerializer.new(root, include: ["parent.wanted"]).serializable_hash + expect(relationship(hash, :parent, "10", :unwanted)).not_to have_key(:data) + end + + it "leaves relationships without lazy_load_data untouched" do + record = LazyLoadDataNestedSpec::Record.new( + id: "1", + leaf: LazyLoadDataNestedSpec::Record.new(id: "9", name: "x") + ) + hash = LazyLoadDataNestedSpec::EagerSerializer.new(record).serializable_hash + expect(hash.dig(:data, :relationships, :leaf)).to have_key(:data) + end + + context "with a deep (3-level) include path" do + let(:tree) do + r = LazyLoadDataNestedSpec::Record + r.new(id: "1", name: "a", child: + r.new(id: "2", name: "b", child: + r.new(id: "3", name: "c", child: + r.new(id: "4", name: "d", child: nil)))) + end + + let(:hash) do + LazyLoadDataNestedSpec::TreeSerializer.new(tree, include: ["child.child.child"]).serializable_hash + end + + it "emits linkage at every requested level" do + expect(relationship(hash, :tree, "1", :child)).to eq(data: {id: "2", type: :tree}) + expect(relationship(hash, :tree, "2", :child)).to eq(data: {id: "3", type: :tree}) + expect(relationship(hash, :tree, "3", :child)).to eq(data: {id: "4", type: :tree}) + end + + it "stops emitting linkage once the include path is exhausted" do + expect(relationship(hash, :tree, "4", :child)).not_to have_key(:data) + end + + it "sideloads every node along the path" do + expect(included_of(hash, :tree).map { |r| r[:id] }).to contain_exactly("2", "3", "4") + end + end + + context "when a record is referenced by two relationships" do + let(:record) do + LazyLoadDataNestedSpec::Record.new( + id: "1", + shared: LazyLoadDataNestedSpec::Record.new(id: "500", name: "shared") + ) + end + + let(:hash) do + LazyLoadDataNestedSpec::SharedRefSerializer.new(record, include: ["first", "second"]).serializable_hash + end + + it "sideloads the shared record only once" do + expect(included_of(hash, :leaf).map { |r| r[:id] }).to eq(["500"]) + end + + it "emits linkage on both relationships" do + expect(hash.dig(:data, :relationships, :first)).to eq(data: {id: "500", type: :leaf}) + expect(hash.dig(:data, :relationships, :second)).to eq(data: {id: "500", type: :leaf}) + end + end + + context "with a nested has_many relationship" do + let(:record) do + r = LazyLoadDataNestedSpec::Record + r.new(id: "1", child: + r.new(id: "10", + leaves: [r.new(id: "100", name: "a"), r.new(id: "101", name: "b")], + others: [r.new(id: "200", name: "c")])) + end + + let(:hash) do + LazyLoadDataNestedSpec::CollectionRootSerializer.new(record, include: ["child.leaves"]).serializable_hash + end + + it "emits data for the requested has_many" do + expect(relationship(hash, :collection, "10", :leaves)).to eq( + data: [{id: "100", type: :leaf}, {id: "101", type: :leaf}] + ) + end + + it "omits data for an unrequested sibling has_many" do + expect(relationship(hash, :collection, "10", :others)).not_to have_key(:data) + end + + it "sideloads only the requested collection's members" do + expect(included_of(hash, :leaf).map { |r| r[:id] }).to contain_exactly("100", "101") + end + end +end + +RSpec.describe "vendored FastJsonapi::SerializationCore" do + it "matches the upstream version get_included_records was copied from" do + expect(Gem.loaded_specs["jsonapi-serializer"].version.to_s).to(eq("2.2.0"), <<~MSG) + SerializationCorePatch#get_included_records is a verbatim copy of + jsonapi-serializer 2.2.0. This spec fails on a version bump so the copy + gets re-diffed against upstream before shipping. + MSG + end +end diff --git a/spec/serializers/alchemy/json_api/node_serializer_spec.rb b/spec/serializers/alchemy/json_api/node_serializer_spec.rb index 6498ff18..3e3a87f5 100644 --- a/spec/serializers/alchemy/json_api/node_serializer_spec.rb +++ b/spec/serializers/alchemy/json_api/node_serializer_spec.rb @@ -32,6 +32,8 @@ subject { serializer.serializable_hash[:data][:relationships] } context "with children" do + let(:options) { {include: [:children]} } + let(:node) do FactoryBot.create( :alchemy_node, @@ -52,6 +54,8 @@ end context "with page assigned" do + let(:options) { {include: [:page]} } + let(:node) do FactoryBot.create( :alchemy_node, diff --git a/spec/serializers/alchemy/json_api/page_serializer_spec.rb b/spec/serializers/alchemy/json_api/page_serializer_spec.rb index 22a83da5..5785ec8e 100644 --- a/spec/serializers/alchemy/json_api/page_serializer_spec.rb +++ b/spec/serializers/alchemy/json_api/page_serializer_spec.rb @@ -98,7 +98,11 @@ subject { serializer.serializable_hash[:data][:relationships] } + let(:options) { {include: [:ancestors]} } + describe "elements" do + let(:options) { {include: [:language]} } + it "does not include trashed, fixed or hidden elements" do expect(subject[:elements]).to eq( data: [ @@ -110,6 +114,8 @@ end describe "fixed_elements" do + let(:options) { {include: [:language]} } + it "does not include trashed, non-fixed or hidden elements" do expect(subject[:fixed_elements]).to eq( data: [