Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Optimize elements loading #31

Merged
merged 4 commits into from
Jan 21, 2021
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
1 change: 1 addition & 0 deletions alchemy-json_api.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,5 @@ Gem::Specification.new do |spec|
spec.add_development_dependency "github_changelog_generator"
spec.add_development_dependency "jsonapi-rspec"
spec.add_development_dependency "rspec-rails"
spec.add_development_dependency "shoulda-matchers"
end
10 changes: 6 additions & 4 deletions app/controllers/alchemy/json_api/pages_controller.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# frozen_string_literal: true

module Alchemy
module JsonApi
class PagesController < JsonApi::BaseController
Expand Down Expand Up @@ -34,6 +35,7 @@ def load_page
end

def load_page_by_id
return unless params[:path] =~ /\A\d+\z/
page_scope.find_by(id: params[:path])
end

Expand All @@ -48,15 +50,15 @@ def page_scope
def page_scope_with_includes
base_page_scope.
with_language(Language.current).
preload(language: {nodes: [:parent, :page]}, all_elements: [:parent_element, :nested_elements, { contents: { essence: :ingredient_association } }])
preload(language: { nodes: [:parent, :page] }, all_elements: { contents: { essence: :ingredient_association } })
end

def base_page_scope
# cancancan is not able to merge our complex AR scopes for logged in users
if can?(:edit_content, Page)
Page.all
if can?(:edit_content, ::Alchemy::Page)
::Alchemy::JsonApi::Page.all
else
Page.published
::Alchemy::JsonApi::Page.published
end
end

Expand Down
33 changes: 33 additions & 0 deletions app/models/alchemy/json_api/element.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
module Alchemy
module JsonApi
class Element < BaseRecord
include Alchemy::Element::Definitions
include Alchemy::Element::ElementContents

self.table_name = "alchemy_elements"

belongs_to :page, class_name: "Alchemy::JsonApi::Page", inverse_of: :all_elements
has_many :contents, class_name: "Alchemy::Content", inverse_of: :element

scope :available, -> { where(public: true).where.not(position: nil) }

def parent_element
page.elements.detect do |element|
element.id == parent_element_id
end
end

def nested_elements
@_nested_elements ||= begin
page.elements.select do |element|
element.parent_element_id == id
end
end
end

def nested_element_ids
nested_elements.map(&:id)
end
end
end
end
48 changes: 48 additions & 0 deletions app/models/alchemy/json_api/page.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
module Alchemy
module JsonApi
class Page < BaseRecord
self.table_name = "alchemy_pages"

belongs_to :language, class_name: "Alchemy::Language"

has_many :all_elements,
-> { available.order(:position) },
class_name: "Alchemy::JsonApi::Element",
inverse_of: :page

scope :published, -> {
where("#{table_name}.public_on <= :time AND " \
"(#{table_name}.public_until IS NULL " \
"OR #{table_name}.public_until >= :time)", time: Time.current)
}

scope :contentpages, -> { where(layoutpage: false) }
scope :layoutpages, -> { where(layoutpage: true) }
scope :with_language, ->(language_id) { where(language_id: language_id) }

# The top level public, non-fixed elements of this page that - if present -
# contains their nested_elements.
def elements
@_elements ||= all_elements.select do |element|
!element.fixed? || element.parent_element_id.nil?
end
end

# The top level public, fixed elements of this page that - if present -
# contains their nested_elements.
def fixed_elements
@_fixed_elements ||= all_elements.select do |element|
element.fixed? || element.parent_element_id.nil?
end
end

def element_ids
@_element_ids ||= elements.map(&:id)
end

def fixed_element_ids
@_fixed_element_ids ||= fixed_elements.map(&:id)
end
end
end
end
17 changes: 12 additions & 5 deletions app/serializers/alchemy/json_api/page_serializer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ module JsonApi
class PageSerializer
include JSONAPI::Serializer

ELEMENT_SERIALIZER = ::Alchemy::JsonApi::ElementSerializer

attributes(
:name,
:urlname,
Expand All @@ -18,12 +20,17 @@ class PageSerializer

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

has_many :elements, record_type: :element, serializer: ::Alchemy::JsonApi::ElementSerializer
has_many :fixed_elements, record_type: :element, serializer: ::Alchemy::JsonApi::ElementSerializer
# 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
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.
has_many :elements, record_type: :element, serializer: ELEMENT_SERIALIZER

has_many :all_elements, record_type: :element, serializer: ::Alchemy::JsonApi::ElementSerializer do |page|
page.all_elements.select { |e| e.public? && !e.trashed? }
end
# The top level public, fixed elements of this page that - if present -
# contains their nested_elements.
has_many :fixed_elements, record_type: :element, serializer: ELEMENT_SERIALIZER
end
end
end
63 changes: 63 additions & 0 deletions spec/models/alchemy/json_api/element_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
require "rails_helper"
require "alchemy/test_support/factories"

RSpec.describe Alchemy::JsonApi::Element, type: :model do
it { should belong_to(:page).class_name("Alchemy::JsonApi::Page") }
it { should have_many(:contents).class_name("Alchemy::Content") }

describe "scopes" do
describe ".available" do
subject(:available) { described_class.available.map(&:id) }

let!(:public_one) { FactoryBot.create(:alchemy_element, public: true) }
let!(:non_public) { FactoryBot.create(:alchemy_element, public: false) }
let!(:trashed) { FactoryBot.create(:alchemy_element, public: true).tap(&:trash!) }

it "returns public available elements" do
# expecting the ids here because the factorys class is not our decorator class
expect(available).to include(public_one.id)
expect(available).to_not include(non_public.id)
expect(available).to_not include(trashed.id)
end
end
end

describe "#parent_element" do
subject { nested_element.parent_element }

let(:page) { FactoryBot.create(:alchemy_page) }
let!(:element) { FactoryBot.create(:alchemy_element, page: page) }
let!(:nested_element) { FactoryBot.create(:alchemy_element, page: page, parent_element: element) }
let!(:not_nested_element) { FactoryBot.create(:alchemy_element, page: page) }

it "returns elements parent element" do
is_expected.to eq(element)
end
end

describe "#nested_elements" do
subject { element.nested_elements }

let(:page) { FactoryBot.create(:alchemy_page) }
let!(:element) { FactoryBot.create(:alchemy_element, page: page) }
let!(:nested_element) { FactoryBot.create(:alchemy_element, page: page, parent_element: element) }
let!(:not_nested_element) { FactoryBot.create(:alchemy_element, page: page) }

it "returns all nested elements" do
is_expected.to eq([nested_element])
end
end

describe "#nested_element_ids" do
subject { element.nested_element_ids }

let(:page) { FactoryBot.create(:alchemy_page) }
let!(:element) { FactoryBot.create(:alchemy_element, page: page) }
let!(:nested_element) { FactoryBot.create(:alchemy_element, page: page, parent_element: element) }
let!(:not_nested_element) { FactoryBot.create(:alchemy_element, page: page) }

it "returns all nested element ids" do
is_expected.to eq([nested_element.id])
end
end
end
Loading