alchemy-json_api 8.2.1 → 8.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: db35a5d154653883deecbcc687597d88f6379f0d0d0374614d01c39d6c9584f3
4
- data.tar.gz: 4214bdae6140ef97bd196573ca8565c4d5fded379ee79c6b12566f3cb61a0f27
3
+ metadata.gz: 19d116a1abd43f5bf4c00da09863cfdb28f2db3452dae76c6490519c144d0858
4
+ data.tar.gz: 91fe9df9c85dc9bb2982045a51191cbe64c03ac68425859ed0249dfe11c7ae50
5
5
  SHA512:
6
- metadata.gz: d699c4291b17ad92015adb2e8987a6328265ef4f1dddb1b76ba191634fd94bb1234379c749bf5a0de49422973cf915b70775dc3c6ca0852637cc07d14e74a40b
7
- data.tar.gz: 17176fb1926e846e4567c9f6243efa877d2c3f5dbef99b65611476bf8a67afd45f29b02cedceb81925e1efcb2d5b6795254667e8c41e5c28bb8243b58443a39d
6
+ metadata.gz: c77e4838a15daaaec7884190d1e60552a76a4af744aab59f1e78c78a4189a0205fb8ec794b15e219b7296982f396400e87b63ecf20273adba93e296fb8330bcd
7
+ data.tar.gz: d974acb9954a11dc9b60a3c401dc1353b47f4fbef9720d7f879d29c060bf7c8f4be6e3861aad6ae347e5544787442cac8a9d3b9f7498459711071dd36d4941f1
data/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # Alchemy::JsonApi
2
2
 
3
+ [![Test](https://github.com/AlchemyCMS/alchemy-json_api/actions/workflows/ci.yml/badge.svg)](https://github.com/AlchemyCMS/alchemy-json_api/actions/workflows/ci.yml) [![JavaScript](https://github.com/AlchemyCMS/alchemy-json_api/actions/workflows/javascript.yml/badge.svg)](https://github.com/AlchemyCMS/alchemy-json_api/actions/workflows/javascript.yml)
4
+
3
5
  A JSON-API based API for AlchemyCMS
4
6
 
5
7
  ## Installation
@@ -26,13 +28,9 @@ $ gem install alchemy-json_api
26
28
 
27
29
  ### In your JS/Frontend app
28
30
 
29
- Run this in your application:
30
-
31
- ```
32
- npm install @alchemy_cms/json_api --save
33
- ```
34
-
35
- or with the package manager of your choice
31
+ A JavaScript/TypeScript deserializer is published as
32
+ [`@alchemy_cms/json_api`](https://www.npmjs.com/package/@alchemy_cms/json_api).
33
+ See [`javascript/README.md`](javascript/README.md) for installation and usage.
36
34
 
37
35
  ## Usage
38
36
 
@@ -47,20 +45,6 @@ mount Alchemy::JsonApi::Engine => "/jsonapi/"
47
45
 
48
46
  > __NOTE__ Pick any path you like. This will be the **prefix** of your API URLs
49
47
 
50
- ### In your frontend app
51
-
52
- This repo provides an NPM package with deserializers to help you convert the response into JS objects.
53
-
54
- ```js
55
- import { deserializePages } from "@alchemy_cms/json_api"
56
-
57
- const response = await fetch("/jsonapi/pages.json")
58
- const data = await response.json()
59
- const pages = deserializePages(data)
60
-
61
- console.log(pages[0].name) // => Homepage
62
- ```
63
-
64
48
  ## HTTP Caching
65
49
 
66
50
  Alchemy::JsonApi allows for caching API responses. It respects the caching configuration of your Rails app and of your Alchemy configuration and settings in the pages page layout configuration. Restricted pages are never cached.
@@ -4,6 +4,8 @@ module Alchemy
4
4
  module JsonApi
5
5
  module Admin
6
6
  class LayoutPagesController < JsonApi::Admin::PagesController
7
+ include FindLayoutPageByPageLayout
8
+
7
9
  private
8
10
 
9
11
  def page_scope
@@ -3,6 +3,8 @@
3
3
  module Alchemy
4
4
  module JsonApi
5
5
  class LayoutPagesController < JsonApi::PagesController
6
+ include FindLayoutPageByPageLayout
7
+
6
8
  private
7
9
 
8
10
  def page_scope
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alchemy
4
+ module JsonApi
5
+ # Lets the layout pages show endpoints find a page by its page layout
6
+ # name in addition to its id or urlname, e.g. +/jsonapi/layout_pages/footer+.
7
+ #
8
+ # Layout pages are usually referenced by their page layout, because their
9
+ # urlname is derived from a name editors can change. Before, clients had
10
+ # to use the index endpoint with a +page_layout_eq+ filter to get them.
11
+ #
12
+ # Lookups by id and urlname take precedence. If more than one layout page
13
+ # with the same page layout exists in a language (i.e. the layout is not
14
+ # +unique+), the first one is returned.
15
+ module FindLayoutPageByPageLayout
16
+ private
17
+
18
+ def load_page_for_cache_key
19
+ super
20
+ rescue ActiveRecord::RecordNotFound
21
+ @page = page_scope.find_by!(page_layout: params[:path])
22
+ end
23
+
24
+ def load_page_by_urlname
25
+ super || page_scope_with_includes.find_by(page_layout: params[:path])
26
+ end
27
+ end
28
+ end
29
+ end
@@ -6,9 +6,7 @@ module Alchemy
6
6
  attributes(
7
7
  :name,
8
8
  :fixed,
9
- :position,
10
- :created_at,
11
- :updated_at
9
+ :position
12
10
  )
13
11
 
14
12
  cache_options store: Rails.cache, namespace: "alchemy-jsonapi"
@@ -18,10 +16,13 @@ module Alchemy
18
16
  end
19
17
 
20
18
  has_many :ingredients,
19
+ lazy_load_data: true,
21
20
  serializer: ->(record) do
22
21
  "Alchemy::JsonApi::Ingredient#{record.type.demodulize}Serializer".constantize
23
22
  end
24
23
 
24
+ # Eager: the element tree (all_elements -> nested_elements) is recursive
25
+ # and can't be expressed as an `include`, so its linkage is always present.
25
26
  has_many :nested_elements, record_type: :element, serializer: self
26
27
  end
27
28
  end
@@ -11,7 +11,7 @@ module Alchemy
11
11
  ingredient.node&.name
12
12
  end
13
13
 
14
- belongs_to :node, record_type: :node, serializer: ::Alchemy::JsonApi::NodeSerializer
14
+ belongs_to :node, record_type: :node, serializer: ::Alchemy::JsonApi::NodeSerializer, lazy_load_data: true
15
15
 
16
16
  with_options if: proc { |ingredient| ingredient.node } do
17
17
  attribute :name do |ingredient|
@@ -19,7 +19,7 @@ module Alchemy
19
19
  ingredient.page&.url_path
20
20
  end
21
21
 
22
- has_one :page, record_type: :page, serializer: PageSerializer do |ingredient|
22
+ has_one :page, record_type: :page, serializer: PageSerializer, lazy_load_data: true do |ingredient|
23
23
  Alchemy::JsonApi::Page.new(ingredient.page) if ingredient.page
24
24
  end
25
25
  end
@@ -10,13 +10,13 @@ module Alchemy
10
10
  :locale
11
11
  )
12
12
 
13
- has_many :menu_items, record_type: :node, serializer: ::Alchemy::JsonApi::NodeSerializer, object_method_name: :nodes, id_method_name: :node_ids
13
+ has_many :menu_items, record_type: :node, serializer: ::Alchemy::JsonApi::NodeSerializer, object_method_name: :nodes, id_method_name: :node_ids, lazy_load_data: true
14
14
 
15
- has_many :menus, record_type: :node, serializer: ::Alchemy::JsonApi::NodeSerializer do |language|
15
+ has_many :menus, record_type: :node, serializer: ::Alchemy::JsonApi::NodeSerializer, lazy_load_data: true do |language|
16
16
  language.nodes.select { |n| n.parent.nil? }
17
17
  end
18
- has_many :pages
19
- has_one :root_page, record_type: :page, serializer: ::Alchemy::JsonApi::PageSerializer
18
+ has_many :pages, lazy_load_data: true
19
+ has_one :root_page, record_type: :page, serializer: ::Alchemy::JsonApi::PageSerializer, lazy_load_data: true
20
20
  end
21
21
  end
22
22
  end
@@ -8,18 +8,19 @@ module Alchemy
8
8
  attribute :link_title, &:title
9
9
  attribute :link_nofollow, &:nofollow
10
10
 
11
- belongs_to :parent, record_type: :node, serializer: self
11
+ belongs_to :parent, record_type: :node, serializer: self, lazy_load_data: true
12
12
 
13
13
  belongs_to(
14
14
  :page,
15
15
  record_type: :page,
16
16
  if: ->(node) { node.page },
17
- serializer: ::Alchemy::JsonApi::PageSerializer
17
+ serializer: ::Alchemy::JsonApi::PageSerializer,
18
+ lazy_load_data: true
18
19
  ) do |node|
19
20
  ::Alchemy::JsonApi::Page.new(node.page)
20
21
  end
21
22
 
22
- has_many :children, record_type: :node, serializer: self
23
+ has_many :children, record_type: :node, serializer: self, lazy_load_data: true
23
24
  end
24
25
  end
25
26
  end
@@ -13,7 +13,9 @@ module Alchemy
13
13
  :language_code,
14
14
  :created_at,
15
15
  :updated_at,
16
- :restricted
16
+ :restricted,
17
+ :robot_index,
18
+ :robot_follow
17
19
  )
18
20
 
19
21
  cache_options store: Rails.cache, namespace: "alchemy-jsonapi"
@@ -33,24 +35,28 @@ module Alchemy
33
35
  end
34
36
  end
35
37
 
36
- belongs_to :language, record_type: :language, serializer: ::Alchemy::JsonApi::LanguageSerializer
38
+ belongs_to :language, record_type: :language, serializer: ::Alchemy::JsonApi::LanguageSerializer, lazy_load_data: true
37
39
 
38
- has_many :ancestors, record_type: :page, serializer: self do |page|
40
+ has_many :ancestors, record_type: :page, serializer: self, lazy_load_data: true do |page|
39
41
  page.ancestors.map do |ancestor|
40
42
  Alchemy::JsonApi::Page.new(ancestor, page_version_type: page.page_version_type)
41
43
  end
42
44
  end
43
45
 
44
46
  # All public elements of this page regardless of if they are fixed or nested.
45
- # Used for eager loading and should be used as the +include+ parameter of your query
47
+ # Used for eager loading and should be used as the +include+ parameter of your query.
48
+ # Eager: paired with the recursive nested_elements linkage to build the element
49
+ # tree, which can't be expressed as an `include`.
46
50
  has_many :all_elements, record_type: :element, serializer: ELEMENT_SERIALIZER
47
51
 
48
52
  # The top level public, non-fixed elements of this page that - if present -
49
- # contains their nested_elements.
53
+ # contains their nested_elements. Eager: the roots of the element tree
54
+ # (see all_elements above).
50
55
  has_many :elements, record_type: :element, serializer: ELEMENT_SERIALIZER
51
56
 
52
57
  # The top level public, fixed elements of this page that - if present -
53
- # contains their nested_elements.
58
+ # contains their nested_elements. Eager: the roots of the element tree
59
+ # (see all_elements above).
54
60
  has_many :fixed_elements, record_type: :element, serializer: ELEMENT_SERIALIZER
55
61
  end
56
62
  end
@@ -1,6 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "jsonapi"
4
+ # Fixes jsonapi-serializer's `lazy_load_data` for nested includes. Loaded at
5
+ # boot (rather than via an autoloader) because it patches a gem constant.
6
+ require "alchemy/json_api/patches/fast_jsonapi/serialization_core_patch"
4
7
 
5
8
  module Alchemy
6
9
  module JsonApi
@@ -6,12 +6,11 @@ module Alchemy
6
6
  def self.included(klass)
7
7
  klass.attributes(
8
8
  :role,
9
- :value,
10
- :created_at,
11
- :updated_at
9
+ :value
12
10
  )
13
11
 
14
12
  klass.attribute :deprecated, &:deprecated?
13
+ klass.cache_options store: Rails.cache, namespace: "alchemy-jsonapi"
15
14
  end
16
15
  end
17
16
  end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "jsonapi/serializer"
4
+
5
+ module Alchemy
6
+ module JsonApi
7
+ module Patches
8
+ module FastJsonapi
9
+ # Makes jsonapi-serializer's `lazy_load_data` honour *nested* includes.
10
+ #
11
+ # With `lazy_load_data: true` a relationship only emits its linkage when
12
+ # it was requested, which upstream (jsonapi-serializer 2.2.0) decides
13
+ # with `includes_list.include?(key)`. Two things break that for any
14
+ # sideloaded (nested) resource:
15
+ #
16
+ # 1. the includes are dotted paths (`primary_taxon.ancestors`), so
17
+ # `include?(:primary_taxon)` never matches; and
18
+ # 2. `get_included_records` hands each sideloaded record the *parent's*
19
+ # includes_list instead of the scoped remainder, so a nested
20
+ # resource judges its own relationships against the wrong paths.
21
+ #
22
+ # Together these suppress linkage for *every* relationship of a nested
23
+ # resource -- even requested ones -- making lazy_load_data unusable
24
+ # beyond top-level relationships.
25
+ #
26
+ # The two overrides below fix each half: `relationships_hash` matches the
27
+ # base key of every requested path, and `get_included_records` threads
28
+ # the scoped remainder into each sideloaded record. Relationships that do
29
+ # not set `lazy_load_data` are unaffected (their linkage always emits).
30
+ # Prepended onto the shared SerializationCore, so every serializer gets it.
31
+ module SerializationCorePatch
32
+ # Upstream tests `includes_list.include?(key)` verbatim. Normalising the
33
+ # list to the set of base keys first makes that check match dotted paths,
34
+ # so the rest of upstream's implementation can stand.
35
+ def relationships_hash(record, relationships = nil, fieldset = nil, includes_list = nil, params = {})
36
+ super(record, relationships, fieldset, base_include_keys(includes_list), params)
37
+ end
38
+
39
+ # Verbatim copy of FastJsonapi::SerializationCore 2.2.0 with a single
40
+ # change, flagged inline below -- keep it diffable against upstream.
41
+ def get_included_records(record, includes_list, known_included_objects, fieldsets, params = {})
42
+ return unless includes_list.present?
43
+ return [] unless relationships_to_serialize
44
+
45
+ includes_list = parse_includes_list(includes_list)
46
+
47
+ includes_list.each_with_object([]) do |include_item, included_records|
48
+ relationship_item = relationships_to_serialize[include_item.first]
49
+
50
+ next unless relationship_item&.include_relationship?(record, params)
51
+
52
+ included_objects = Array(relationship_item.fetch_associated_object(record, params))
53
+ next if included_objects.empty?
54
+
55
+ static_serializer = relationship_item.static_serializer
56
+ static_record_type = relationship_item.static_record_type
57
+
58
+ included_objects.each do |inc_obj|
59
+ serializer = static_serializer || relationship_item.serializer_for(inc_obj, params)
60
+ record_type = static_record_type || serializer.record_type
61
+
62
+ if include_item.last.any?
63
+ serializer_records = serializer.get_included_records(inc_obj, include_item.last, known_included_objects, fieldsets, params)
64
+ included_records.concat(serializer_records) unless serializer_records.empty?
65
+ end
66
+
67
+ code = "#{record_type}_#{serializer.id_from_record(inc_obj, params)}"
68
+ next if known_included_objects.include?(code)
69
+
70
+ known_included_objects << code
71
+
72
+ # CHANGED vs upstream: pass this include's remainder rather than
73
+ # the parent's parsed includes_list, so the sideloaded record
74
+ # scopes its own relationships correctly.
75
+ included_records << serializer.record_hash(inc_obj, fieldsets[record_type], include_item.last, params)
76
+ end
77
+ end
78
+ end
79
+
80
+ private
81
+
82
+ # The base relationship keys requested at this level, e.g.
83
+ # `[:primary_taxon.ancestors, :author]` -> Set[:primary_taxon, :author].
84
+ def base_include_keys(includes_list)
85
+ return Set.new if includes_list.blank?
86
+
87
+ includes_list.map { |path| path.to_s.split(".", 2).first.to_sym }.to_set
88
+ end
89
+
90
+ ::FastJsonapi::SerializationCore::ClassMethods.prepend(self)
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
@@ -5,14 +5,8 @@ RSpec.shared_examples "an ingredient serializer" do
5
5
  subject { serializer.serializable_hash[:data][:attributes] }
6
6
 
7
7
  it "has the right transformed keys and values" do
8
- transform_method = FastJsonapi::ObjectSerializer::TRANSFORMS_MAPPING[Alchemy::JsonApi.key_transform]
9
- transformed_keys = %w[created_at updated_at].map { |k| k.send(*transform_method).to_sym }
10
-
11
8
  expect(subject).to have_key(:role)
12
9
  expect(subject).to have_key(:value)
13
- transformed_keys.each do |transformed_key|
14
- expect(subject).to have_key(transformed_key)
15
- end
16
10
  expect(subject[:deprecated]).to be(false)
17
11
  end
18
12
 
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Alchemy
4
4
  module JsonApi
5
- VERSION = "8.2.1"
5
+ VERSION = "8.4.0"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: alchemy-json_api
3
3
  version: !ruby/object:Gem::Version
4
- version: 8.2.1
4
+ version: 8.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Martin Meyerhoff
@@ -16,7 +16,7 @@ dependencies:
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: 8.2.0.a
19
+ version: 8.3.0
20
20
  - - "<"
21
21
  - !ruby/object:Gem::Version
22
22
  version: '9'
@@ -26,7 +26,7 @@ dependencies:
26
26
  requirements:
27
27
  - - ">="
28
28
  - !ruby/object:Gem::Version
29
- version: 8.2.0.a
29
+ version: 8.3.0
30
30
  - - "<"
31
31
  - !ruby/object:Gem::Version
32
32
  version: '9'
@@ -122,6 +122,7 @@ files:
122
122
  - app/controllers/alchemy/json_api/layout_pages_controller.rb
123
123
  - app/controllers/alchemy/json_api/nodes_controller.rb
124
124
  - app/controllers/alchemy/json_api/pages_controller.rb
125
+ - app/controllers/concerns/alchemy/json_api/find_layout_page_by_page_layout.rb
125
126
  - app/models/alchemy/json_api/page.rb
126
127
  - app/serializers/alchemy/json_api/base_serializer.rb
127
128
  - app/serializers/alchemy/json_api/element_serializer.rb
@@ -147,6 +148,7 @@ files:
147
148
  - lib/alchemy/json_api.rb
148
149
  - lib/alchemy/json_api/engine.rb
149
150
  - lib/alchemy/json_api/ingredient_serializer.rb
151
+ - lib/alchemy/json_api/patches/fast_jsonapi/serialization_core_patch.rb
150
152
  - lib/alchemy/json_api/test_support/ingredient_serializer_behaviour.rb
151
153
  - lib/alchemy/json_api/version.rb
152
154
  - lib/tasks/alchemy/json_api_tasks.rake