forest_liana 9.22.4 → 9.22.5

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: f8365b7c92d4157a34acd7f00af2c3d515976b96884eef48c0a0e72c775b1ea0
4
- data.tar.gz: a61cfdd476921b56e293014854693c3aa39ffd6d8474544273be222f91c5d0db
3
+ metadata.gz: 6ca262c2db5e774393d0dd9b9266bbd360cea72c8caeea733c7df95a32bd6f3c
4
+ data.tar.gz: 5917abff7f56a4df5bf38c885ab375dc7240f666826c46a5ddb92a4709110d8e
5
5
  SHA512:
6
- metadata.gz: 8ee81334205b47404f4fc9349fcde08b8d38be877a525a1cfb8bc1481de1b435d88b0724d48552cbc111735a751b2f84c60a49cc4142a5a7c3a9fab6df201d49
7
- data.tar.gz: dec06373edbb86308bb9577c0f20d61a747ad9fa0fac691b1f70fadada3d2e27406337b188eda61f10c5ad96405c6d65db77bd4e5144dd079e9445d1b1a13cc4
6
+ metadata.gz: c60d6abc5f591ac151cf592e56f7790fc24db53984f4a646da46bf61ca1030b77d21713a4ee7d63a334ad9a70c5b934c982ce1fd76669e485be2c58e634fddcf
7
+ data.tar.gz: 55b8f9f4a71a17dcb8cb3c55b0a240e0576697ae5ca42116f266aa9b429d15aea49193222ea0f599a6feb892405f3fdfb565e1d1a033e4595c22f351e277e112
@@ -544,8 +544,13 @@ module ForestLiana
544
544
  # Skip ActiveStorage associations - already processed above
545
545
  next if is_active_storage_association?(association)
546
546
 
547
+ # NOTICE: A preloaded :through only reads keys off the rows this query builds: the root
548
+ # row, and the narrowed rows of a first hop it joins. Walking the whole chain would
549
+ # name tables it never joins.
550
+ if through_chain.any? && !joined?(association, joined_relations)
551
+ select_preload_hop_keys(select, [association.name], joined_relations)
547
552
  # For :through associations, recursively add all intermediate foreign keys
548
- if through_chain.any?
553
+ elsif through_chain.any?
549
554
  current_resource = projected_resource
550
555
  through_chain.reverse.each do |through_name|
551
556
  through_assoc = current_resource.reflect_on_association(through_name)
@@ -694,7 +699,11 @@ module ForestLiana
694
699
  # those rows are the narrowed ones built off the JOIN, so its key has to be named here too.
695
700
  # It was not, and the list answered 500 rather than the one field (PRD-1316).
696
701
  def select_dependency_preload_keys(select, relation_path, joined_relations)
697
- chains = flatten_dependency_hops(relation_path.relations)
702
+ select_preload_hop_keys(select, relation_path.relations, joined_relations)
703
+ end
704
+
705
+ def select_preload_hop_keys(select, relations, joined_relations)
706
+ chains = flatten_dependency_hops(relations)
698
707
  return if chains.empty?
699
708
 
700
709
  narrowed_hops(chains, joined_relations).each do |owner, reflection|
@@ -1,3 +1,3 @@
1
1
  module ForestLiana
2
- VERSION = "9.22.4"
2
+ VERSION = "9.22.5"
3
3
  end
@@ -0,0 +1,8 @@
1
+ class Boat < ActiveRecord::Base
2
+ belongs_to :harbor
3
+ belongs_to :captain
4
+
5
+ # Its second hop is keyed on captains.license_number, which the preloader reads off the captain
6
+ # rows: narrowed ones when Harbor's boats related list joins captain to sort on it.
7
+ has_one :captain_license, through: :captain, source: :license
8
+ end
@@ -0,0 +1,3 @@
1
+ class Captain < ActiveRecord::Base
2
+ has_one :license, primary_key: :license_number, foreign_key: :number
3
+ end
@@ -0,0 +1,3 @@
1
+ class Harbor < ActiveRecord::Base
2
+ has_many :boats
3
+ end
@@ -0,0 +1,2 @@
1
+ class License < ActiveRecord::Base
2
+ end
@@ -1,4 +1,9 @@
1
1
  class Membership < ActiveRecord::Base
2
2
  belongs_to :island
3
3
  belongs_to :user
4
+
5
+ # A :through of a :through: displayed on Island's memberships related list, the hop past the
6
+ # first reads `locations`, a table that list preloads rather than joins (issue #818).
7
+ has_one :location, through: :island
8
+ has_one :location_island, through: :location, source: :island
4
9
  end
@@ -0,0 +1,23 @@
1
+ class CreateHarborsBoatsCaptainsLicenses < ActiveRecord::Migration[6.0]
2
+ def change
3
+ create_table :harbors do |t|
4
+ t.string :name
5
+ end
6
+
7
+ create_table :captains do |t|
8
+ t.string :name
9
+ t.string :license_number
10
+ end
11
+
12
+ create_table :licenses do |t|
13
+ t.string :number
14
+ t.date :expires_on
15
+ end
16
+
17
+ create_table :boats do |t|
18
+ t.string :name
19
+ t.references :harbor
20
+ t.references :captain
21
+ end
22
+ end
23
+ end
@@ -10,7 +10,7 @@
10
10
  #
11
11
  # It's strongly recommended that you check this file into your version control system.
12
12
 
13
- ActiveRecord::Schema.define(version: 2026_09_24_120000) do
13
+ ActiveRecord::Schema.define(version: 2026_10_01_120000) do
14
14
 
15
15
  create_table "addresses", force: :cascade do |t|
16
16
  t.string "line1"
@@ -29,6 +29,19 @@ ActiveRecord::Schema.define(version: 2026_09_24_120000) do
29
29
  t.datetime "updated_at", precision: 6, null: false
30
30
  end
31
31
 
32
+ create_table "boats", force: :cascade do |t|
33
+ t.string "name"
34
+ t.integer "harbor_id"
35
+ t.integer "captain_id"
36
+ t.index ["captain_id"], name: "index_boats_on_captain_id"
37
+ t.index ["harbor_id"], name: "index_boats_on_harbor_id"
38
+ end
39
+
40
+ create_table "captains", force: :cascade do |t|
41
+ t.string "name"
42
+ t.string "license_number"
43
+ end
44
+
32
45
  create_table "flags", force: :cascade do |t|
33
46
  t.integer "island_id"
34
47
  t.string "color"
@@ -37,6 +50,10 @@ ActiveRecord::Schema.define(version: 2026_09_24_120000) do
37
50
  t.index ["island_id"], name: "index_flags_on_island_id"
38
51
  end
39
52
 
53
+ create_table "harbors", force: :cascade do |t|
54
+ t.string "name"
55
+ end
56
+
40
57
  create_table "isle", force: :cascade do |t|
41
58
  t.string "name"
42
59
  t.binary "map"
@@ -44,6 +61,11 @@ ActiveRecord::Schema.define(version: 2026_09_24_120000) do
44
61
  t.datetime "updated_at"
45
62
  end
46
63
 
64
+ create_table "licenses", force: :cascade do |t|
65
+ t.string "number"
66
+ t.date "expires_on"
67
+ end
68
+
47
69
  create_table "locations", force: :cascade do |t|
48
70
  t.string "coordinates"
49
71
  t.integer "island_id"
@@ -66,8 +66,12 @@ module ForestLiana
66
66
  [
67
67
  Address,
68
68
  Article,
69
+ Boat,
70
+ Captain,
69
71
  Flag,
72
+ Harbor,
70
73
  Island,
74
+ License,
71
75
  Location,
72
76
  Manufacturer,
73
77
  Membership,
@@ -0,0 +1,55 @@
1
+ require 'rails_helper'
2
+
3
+ describe 'A related list displaying a has_one :through', type: :request do
4
+ before do
5
+ allow(ForestLiana::IpWhitelist).to receive(:retrieve) { true }
6
+ allow(ForestLiana::IpWhitelist).to receive(:is_ip_whitelist_retrieved) { true }
7
+ allow(ForestLiana::IpWhitelist).to receive(:is_ip_valid) { true }
8
+ allow_any_instance_of(ForestLiana::Ability).to receive(:forest_authorize!) { true }
9
+ Rails.cache.write('forest.has_permission', false)
10
+ allow(ForestLiana::ScopeManager).to receive(:fetch_scopes)
11
+ .and_return('scopes' => {}, 'team' => { 'id' => '1', 'name' => 'Operations' })
12
+ end
13
+
14
+ token = JWT.encode({ id: 38, email: 'michael.kelso@that70.show', first_name: 'Michael',
15
+ last_name: 'Kelso', team: 'Operations', rendering_id: 16,
16
+ exp: Time.now.to_i + 2.weeks.to_i, permission_level: 'admin' },
17
+ ForestLiana.auth_secret, 'HS256')
18
+ headers = { 'Accept' => 'application/json', 'Content-Type' => 'application/json',
19
+ 'Authorization' => "Bearer #{token}" }
20
+
21
+ let!(:user) { User.create!(name: 'Michel', title: :king) }
22
+ let!(:island) { Island.create!(name: 'Skull') }
23
+ let!(:location) { Location.create!(coordinates: '12345', island: island) }
24
+ let!(:membership) { Membership.create!(island: island, user: user) }
25
+
26
+ it 'serves a :through going through another :through' do
27
+ get "/forest/Island/#{island.id}/relationships/memberships",
28
+ params: { fields: { 'Membership' => 'id,location_island', 'location_island' => 'name' },
29
+ page: { 'number' => '1', 'size' => '15' }, sort: '-id', timezone: 'Europe/Paris' },
30
+ headers: headers
31
+
32
+ expect(response.status).to eq(200)
33
+ body = JSON.parse(response.body)
34
+ expect(body['data'].map { |row| row.dig('relationships', 'location_island', 'data', 'id') }).to eq([island.id.to_s])
35
+ expect(body['included'].map { |record| record.dig('attributes', 'name') }).to eq(['Skull'])
36
+ end
37
+
38
+ # Sorting on captain joins it with only the columns the list shows, and the preloader reads the
39
+ # license's key, captains.license_number, off those narrowed rows.
40
+ it 'serves a :through whose first hop is joined and keys the next hop on a non-primary column' do
41
+ harbor = Harbor.create!(name: 'Saint-Malo')
42
+ captain = Captain.create!(name: 'Haddock', license_number: 'FR-4421')
43
+ license = License.create!(number: 'FR-4421', expires_on: Date.new(2030, 1, 1))
44
+ Boat.create!(name: 'Karaboudjan', harbor: harbor, captain: captain)
45
+
46
+ get "/forest/Harbor/#{harbor.id}/relationships/boats",
47
+ params: { fields: { 'Boat' => 'id,captain,captain_license', 'captain' => 'name', 'captain_license' => 'number' },
48
+ page: { 'number' => '1', 'size' => '15' }, sort: 'captain.name', timezone: 'Europe/Paris' },
49
+ headers: headers
50
+
51
+ expect(response.status).to eq(200)
52
+ body = JSON.parse(response.body)
53
+ expect(body['data'].map { |row| row.dig('relationships', 'captain_license', 'data', 'id') }).to eq([license.id.to_s])
54
+ end
55
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: forest_liana
3
3
  version: !ruby/object:Gem::Version
4
- version: 9.22.4
4
+ version: 9.22.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sandro Munda
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-09-28 00:00:00.000000000 Z
11
+ date: 2026-10-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -386,11 +386,15 @@ files:
386
386
  - spec/dummy/app/models/address.rb
387
387
  - spec/dummy/app/models/application_record.rb
388
388
  - spec/dummy/app/models/article.rb
389
+ - spec/dummy/app/models/boat.rb
390
+ - spec/dummy/app/models/captain.rb
389
391
  - spec/dummy/app/models/car.rb
390
392
  - spec/dummy/app/models/driver.rb
391
393
  - spec/dummy/app/models/flag.rb
392
394
  - spec/dummy/app/models/garage_record.rb
395
+ - spec/dummy/app/models/harbor.rb
393
396
  - spec/dummy/app/models/island.rb
397
+ - spec/dummy/app/models/license.rb
394
398
  - spec/dummy/app/models/location.rb
395
399
  - spec/dummy/app/models/manufacturer.rb
396
400
  - spec/dummy/app/models/membership.rb
@@ -446,6 +450,7 @@ files:
446
450
  - spec/dummy/db/migrate/20260914120000_create_articles_and_taggings.rb
447
451
  - spec/dummy/db/migrate/20260922120000_create_trees_users.rb
448
452
  - spec/dummy/db/migrate/20260924120000_add_maker_name_to_products.rb
453
+ - spec/dummy/db/migrate/20261001120000_create_harbors_boats_captains_licenses.rb
449
454
  - spec/dummy/db/schema.rb
450
455
  - spec/dummy/db/user_schema.rb
451
456
  - spec/dummy/lib/forest_liana/collections/address.rb
@@ -477,6 +482,7 @@ files:
477
482
  - spec/requests/projection_header_spec.rb
478
483
  - spec/requests/projection_inherited_loads_spec.rb
479
484
  - spec/requests/query_footprint_spec.rb
485
+ - spec/requests/related_list_nested_through_spec.rb
480
486
  - spec/requests/resources_spec.rb
481
487
  - spec/requests/stats_spec.rb
482
488
  - spec/requests/sti_record_conversion_spec.rb
@@ -734,11 +740,15 @@ test_files:
734
740
  - spec/dummy/app/models/address.rb
735
741
  - spec/dummy/app/models/application_record.rb
736
742
  - spec/dummy/app/models/article.rb
743
+ - spec/dummy/app/models/boat.rb
744
+ - spec/dummy/app/models/captain.rb
737
745
  - spec/dummy/app/models/car.rb
738
746
  - spec/dummy/app/models/driver.rb
739
747
  - spec/dummy/app/models/flag.rb
740
748
  - spec/dummy/app/models/garage_record.rb
749
+ - spec/dummy/app/models/harbor.rb
741
750
  - spec/dummy/app/models/island.rb
751
+ - spec/dummy/app/models/license.rb
742
752
  - spec/dummy/app/models/location.rb
743
753
  - spec/dummy/app/models/manufacturer.rb
744
754
  - spec/dummy/app/models/membership.rb
@@ -794,6 +804,7 @@ test_files:
794
804
  - spec/dummy/db/migrate/20260914120000_create_articles_and_taggings.rb
795
805
  - spec/dummy/db/migrate/20260922120000_create_trees_users.rb
796
806
  - spec/dummy/db/migrate/20260924120000_add_maker_name_to_products.rb
807
+ - spec/dummy/db/migrate/20261001120000_create_harbors_boats_captains_licenses.rb
797
808
  - spec/dummy/db/schema.rb
798
809
  - spec/dummy/db/user_schema.rb
799
810
  - spec/dummy/lib/forest_liana/collections/address.rb
@@ -825,6 +836,7 @@ test_files:
825
836
  - spec/requests/projection_header_spec.rb
826
837
  - spec/requests/projection_inherited_loads_spec.rb
827
838
  - spec/requests/query_footprint_spec.rb
839
+ - spec/requests/related_list_nested_through_spec.rb
828
840
  - spec/requests/resources_spec.rb
829
841
  - spec/requests/stats_spec.rb
830
842
  - spec/requests/sti_record_conversion_spec.rb