forest_liana 9.11.3 → 9.12.1

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: dcd40a6f023ba4a2a7eddd15891ac11743dea8fee0df3378c42e604afbfe8c7e
4
- data.tar.gz: 61ceac08bd477c79ea3d6284114b6a9a08f2514758378f4f7a96c3845efd4f8f
3
+ metadata.gz: a205f59494a1a151c8bb1a5be912905bf7f1c9242136ae63de22f0372f7268bf
4
+ data.tar.gz: ddba735dde964ffe46c0dc2907edd065e707a3d63d8283f25b2155c903d3eeb5
5
5
  SHA512:
6
- metadata.gz: e09c8a0a86cee9b238e35057b0e29e5b842fcf0a2a1380a57fb10bf00c9c2cf6cb5cf16bb9908d72fc4219966efa910742de673e5d0f9cd6696bbd83724d4f67
7
- data.tar.gz: 24ecaa8c8a9442ec3c677bb71f47dbf8a120cba326732f202264869d1a3188265a30caa14054ffbd13c218deb6b13294ef997f60d83f0f347faccbc60d32bc98
6
+ metadata.gz: eaaf6fbca9592c4853bcdfa2ec6c5983ee0f468fd62d36aab03cf55dc865a7ae7195c079b8d05a185c174a81cadb834f87e7ff85c69f489336a58ceb65c76559
7
+ data.tar.gz: 3f2dc59f88da2c969f8cf865809232d0b58988cf1f395d59837b2df0d3ef094854274e82eca82c3432d8672d9a1c760dc0204c86347cbdff3ae5304b5f555323
@@ -30,7 +30,20 @@ module ForestLiana
30
30
  end
31
31
 
32
32
  def perform
33
- @records = optimize_record_loading(@resource, @records)
33
+ polymorphic_association, preload_loads = analyze_associations(@resource)
34
+ includes = @includes.uniq - polymorphic_association - preload_loads
35
+ has_smart_fields = @params[:fields][@collection_name].split(',').any? do |field|
36
+ ForestLiana::SchemaHelper.is_smart_field?(@resource, field)
37
+ end
38
+
39
+ if includes.empty? || has_smart_fields
40
+ @records = optimize_record_loading(@resource, @records)
41
+ else
42
+ select = compute_select_fields
43
+ @records = optimize_record_loading(@resource, @records).references(includes).select(*select)
44
+ end
45
+
46
+ @records
34
47
  end
35
48
 
36
49
  def count
@@ -210,5 +223,45 @@ module ForestLiana
210
223
  def pagination?
211
224
  @params[:page]&.dig(:number)
212
225
  end
226
+
227
+ def compute_select_fields
228
+ select = ['_forest_admin_eager_load']
229
+ @params[:fields][@collection_name].split(',').each do |path|
230
+ association = get_one_association(path)
231
+ if association
232
+ while association.options[:through]
233
+ association = get_one_association(association.options[:through])
234
+ end
235
+
236
+ if SchemaUtils.polymorphic?(association)
237
+ select << "#{@resource.table_name}.#{association.foreign_type}"
238
+ end
239
+ select << "#{@resource.table_name}.#{association.foreign_key}"
240
+ end
241
+
242
+ if @params[:fields].key?(path)
243
+ association = get_one_association(path)
244
+ table_name = association.table_name
245
+
246
+ @params[:fields][path].split(',').each do |association_path|
247
+ if ForestLiana::SchemaHelper.is_smart_field?(association.klass, association_path)
248
+ association.klass.attribute_names.each { |attribute| select << "#{table_name}.#{attribute}" }
249
+ else
250
+ select << "#{table_name}.#{association_path}"
251
+ end
252
+ end
253
+ else
254
+ select << "#{@resource.table_name}.#{path}"
255
+ end
256
+ end
257
+
258
+ select.uniq
259
+ end
260
+
261
+ def get_one_association(name)
262
+ ForestLiana::QueryHelper.get_one_associations(@resource)
263
+ .select { |association| association.name == name.to_sym }
264
+ .first
265
+ end
213
266
  end
214
267
  end
@@ -0,0 +1,65 @@
1
+ module ForestLiana
2
+ module ActiveRecordOverride
3
+ module Associations
4
+ require 'active_record/associations/join_dependency'
5
+ module JoinDependency
6
+ def apply_column_aliases(relation)
7
+ if !(@join_root_alias = relation.select_values.empty?) &&
8
+ relation.select_values.first.to_s == '_forest_admin_eager_load'
9
+
10
+ relation.select_values.shift
11
+ used_cols = {}
12
+ # Find and expand out all column names being used in select(...)
13
+ new_select_values = relation.select_values.map(&:to_s).each_with_object([]) do |col, select|
14
+ unless col.include?(' ') # Pass it through if it's some expression (No chance for a simple column reference)
15
+ col = if (col_parts = col.split('.')).length == 1
16
+ [col]
17
+ else
18
+ [col_parts[0..-2].join('.'), col_parts.last]
19
+ end
20
+ used_cols[col] = nil
21
+ end
22
+ select << col
23
+ end
24
+
25
+ if new_select_values.present?
26
+ relation.select_values = new_select_values
27
+ else
28
+ relation.select_values.clear
29
+ end
30
+
31
+ @aliases ||= ActiveRecord::Associations::JoinDependency::Aliases.new(join_root.each_with_index.map do |join_part, i|
32
+ join_alias = join_part.table&.table_alias || join_part.table_name
33
+ keys = [join_part.base_klass.primary_key] # Always include the primary key
34
+
35
+ # # %%% Optional to include all foreign keys:
36
+ # keys.concat(join_part.base_klass.reflect_on_all_associations.select { |a| a.belongs_to? }.map(&:foreign_key))
37
+ # Add foreign keys out to referenced tables that we belongs_to
38
+ join_part.children.each { |child| keys << child.reflection.foreign_key if child.reflection.belongs_to? }
39
+
40
+ # Add the foreign key that got us here -- "the train we rode in on" -- if we arrived from
41
+ # a has_many or has_one:
42
+ if join_part.is_a?(ActiveRecord::Associations::JoinDependency::JoinAssociation) &&
43
+ !join_part.reflection.belongs_to?
44
+ keys << join_part.reflection.foreign_key
45
+ end
46
+ keys = keys.compact # In case we're using composite_primary_keys
47
+ j = 0
48
+ columns = join_part.column_names.each_with_object([]) do |column_name, s|
49
+ # Include columns chosen in select(...) as well as the PK and any relevant FKs
50
+ if used_cols.keys.find { |c| (c.length == 1 || c.first == join_alias) && c.last == column_name } ||
51
+ keys.find { |c| c == column_name }
52
+ s << ActiveRecord::Associations::JoinDependency::Aliases::Column.new(column_name, "t#{i}_r#{j}")
53
+ end
54
+ j += 1
55
+ end
56
+ ActiveRecord::Associations::JoinDependency::Aliases::Table.new(join_part, columns)
57
+ end)
58
+ relation.select_values.clear
59
+ end
60
+ relation._select!(-> { aliases.columns })
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
@@ -7,6 +7,7 @@ require 'jwt'
7
7
  require 'bcrypt'
8
8
  require_relative 'bootstrapper'
9
9
  require_relative 'collection'
10
+ require_relative 'active_record_override'
10
11
 
11
12
  module Rack
12
13
  class Cors
@@ -90,6 +91,12 @@ module ForestLiana
90
91
  end
91
92
  end
92
93
 
94
+ initializer 'forest_liana.override_active_record_dependency' do
95
+ ActiveSupport.on_load(:active_record) do
96
+ ActiveRecord::Associations::JoinDependency.prepend(ForestLiana::ActiveRecordOverride::Associations::JoinDependency)
97
+ end
98
+ end
99
+
93
100
  config.after_initialize do |app|
94
101
  if error
95
102
  FOREST_REPORTER.report error
@@ -1,3 +1,3 @@
1
1
  module ForestLiana
2
- VERSION = "9.11.3"
2
+ VERSION = "9.12.1"
3
3
  end
@@ -7,4 +7,6 @@ class Tree < ActiveRecord::Base
7
7
  class_name: 'Island',
8
8
  inverse_of: :eponymous_tree,
9
9
  optional: true
10
+
11
+ has_one :location, through: :island
10
12
  end
@@ -37,6 +37,7 @@ module ForestLiana
37
37
  { name: :cutter, klass: User },
38
38
  { name: :island, klass: Island },
39
39
  { name: :eponymous_island, klass: Island },
40
+ { name: :location, klass: Location },
40
41
  ]
41
42
  end
42
43
 
@@ -52,13 +53,17 @@ module ForestLiana
52
53
 
53
54
  describe 'get_one_association_names_symbol' do
54
55
  it 'should return the one-one associations names as symbols' do
55
- expect(QueryHelper.get_one_association_names_symbol(Tree)).to eq([:owner, :cutter, :island, :eponymous_island])
56
+ expect(QueryHelper.get_one_association_names_symbol(Tree)).to eq(
57
+ [:owner, :cutter, :island, :eponymous_island, :location]
58
+ )
56
59
  end
57
60
  end
58
61
 
59
62
  describe 'get_one_association_names_string' do
60
63
  it 'should return the one-one associations names as strings' do
61
- expect(QueryHelper.get_one_association_names_string(Tree)).to eq(['owner', 'cutter', 'island', 'eponymous_island'])
64
+ expect(QueryHelper.get_one_association_names_string(Tree)).to eq(
65
+ ['owner', 'cutter', 'island', 'eponymous_island', 'location']
66
+ )
62
67
  end
63
68
  end
64
69
 
@@ -71,12 +76,12 @@ module ForestLiana
71
76
  end
72
77
  end
73
78
 
74
- context 'on a model having 2 belongsTo associations' do
79
+ context 'on a model having 3 belongsTo/hasOne associations' do
75
80
  tables_associated_to_relations_name =
76
81
  QueryHelper.get_tables_associated_to_relations_name(Tree)
77
82
 
78
83
  it 'should return the one-one associations' do
79
- expect(tables_associated_to_relations_name.keys.length).to eq(2)
84
+ expect(tables_associated_to_relations_name.keys.length).to eq(3)
80
85
  end
81
86
 
82
87
  it 'should return relationships having a name different than the targeted model' do
@@ -4,7 +4,9 @@ describe 'Requesting Tree resources', :type => :request do
4
4
  let(:scope_filters) { {'scopes' => {}, 'team' => {'id' => '1', 'name' => 'Operations'}} }
5
5
  before do
6
6
  user = User.create(name: 'Michel')
7
- Tree.create(name: 'Lemon Tree', owner: user, cutter: user)
7
+ tree = Tree.create(name: 'Lemon Tree', owner: user, cutter: user)
8
+ island = Island.create(name: 'Lemon Island', trees: [tree])
9
+ Location.create(coordinates: '1,2', island: island)
8
10
 
9
11
  Rails.cache.write('forest.users', {'1' => { 'id' => 1, 'roleId' => 1, 'rendering_id' => '1' }})
10
12
  Rails.cache.write('forest.has_permission', true)
@@ -32,6 +34,8 @@ describe 'Requesting Tree resources', :type => :request do
32
34
  after do
33
35
  User.destroy_all
34
36
  Tree.destroy_all
37
+ Island.destroy_all
38
+ Location.destroy_all
35
39
  end
36
40
 
37
41
  token = JWT.encode({
@@ -54,7 +58,7 @@ describe 'Requesting Tree resources', :type => :request do
54
58
  describe 'index' do
55
59
  describe 'without any filter' do
56
60
  params = {
57
- fields: { 'Tree' => 'id,name' },
61
+ fields: { 'Tree' => 'id,name,location' },
58
62
  page: { 'number' => '1', 'size' => '10' },
59
63
  searchExtended: '0',
60
64
  sort: '-id',
@@ -98,7 +102,8 @@ describe 'Requesting Tree resources', :type => :request do
98
102
 
99
103
  it 'should respond the tree data' do
100
104
  get '/forest/Tree', params: params, headers: headers
101
- expect(JSON.parse(response.body)).to eq({
105
+
106
+ expect(JSON.parse(response.body)).to include({
102
107
  "data" => [{
103
108
  "type" => "Tree",
104
109
  "id" => "1",
@@ -108,9 +113,30 @@ describe 'Requesting Tree resources', :type => :request do
108
113
  },
109
114
  "links" => {
110
115
  "self" => "/forest/tree/1"
116
+ },
117
+ "relationships" => {
118
+ "location" => {
119
+ "data" => { "id" => "1", "type" => "Location" },
120
+ "links" => { "related" => {} }
121
+ }
111
122
  }
112
123
  }],
113
- "included" => []
124
+ "included" => [{
125
+ "type" => "Location",
126
+ "id" => "1",
127
+ "attributes" => include(
128
+ "id" => 1,
129
+ "created_at" => nil,
130
+ "updated_at" => nil,
131
+ "coordinates" => nil
132
+ ),
133
+ "links" => { "self" => "/forest/location/1" },
134
+ "relationships" => {
135
+ "island" => {
136
+ "links" => { "related" => {} }
137
+ }
138
+ }
139
+ }]
114
140
  })
115
141
  end
116
142
  end
@@ -4,7 +4,7 @@ module ForestLiana
4
4
  let(:pageSize) { 10 }
5
5
  let(:pageNumber) { 1 }
6
6
  let(:sort) { 'id' }
7
- let(:fields) {}
7
+ let(:fields) { { resource.name => 'id' } }
8
8
  let(:filters) {}
9
9
  let(:scopes) { {'scopes' => {}, 'team' => {'id' => '1', 'name' => 'Operations'}} }
10
10
  let(:rendering_id) { 13 }
@@ -190,6 +190,7 @@ module ForestLiana
190
190
 
191
191
  describe 'when on a model having a reserved SQL word as name' do
192
192
  let(:resource) { Reference }
193
+ let(:fields) { { resource.name => 'id' } }
193
194
 
194
195
  it 'should get the ressource properly' do
195
196
  getter.perform
@@ -219,6 +220,7 @@ module ForestLiana
219
220
 
220
221
  describe 'when sorting by a belongs_to association' do
221
222
  let(:resource) { Tree }
223
+ let(:fields) { { resource.name => 'id' } }
222
224
  let(:sort) { 'owner.name' }
223
225
 
224
226
  it 'should get only the expected records' do
@@ -288,6 +290,22 @@ module ForestLiana
288
290
  end
289
291
  end
290
292
 
293
+ describe 'when getting a has_one through association' do
294
+ let(:resource) { Tree }
295
+ let(:fields) { { 'Tree' => 'id,location' } }
296
+
297
+ it 'should get the expected records, including the foreign_key for the direct association' do
298
+ getter.perform
299
+ records = getter.records
300
+ count = getter.count
301
+
302
+ expect(records.count).to eq Tree.count
303
+ expect(count).to eq Tree.count
304
+ expect(records.map(&:id)).to match_array(Tree.pluck(:id))
305
+ expect(records.map(&:island_id)).to match_array(Tree.pluck(:island_id))
306
+ end
307
+ end
308
+
291
309
  describe 'when filtering on an ambiguous field' do
292
310
  let(:resource) { Tree }
293
311
  let(:pageSize) { 5 }
@@ -501,7 +519,7 @@ module ForestLiana
501
519
  describe 'when scopes are defined' do
502
520
  let(:resource) { Island }
503
521
  let(:pageSize) { 15 }
504
- let(:fields) { }
522
+ let(:fields) { { resource.name => 'id' } }
505
523
  let(:filters) { }
506
524
  let(:scopes) {
507
525
  {
@@ -61,7 +61,18 @@ module ForestLiana
61
61
  end
62
62
 
63
63
  expect(collection.fields.map { |field| field[:field].to_s}).to eq(
64
- ["age", "created_at", "cutter", "eponymous_island", "id", "island", "name", "owner", "updated_at"]
64
+ [
65
+ "age",
66
+ "created_at",
67
+ "cutter",
68
+ "eponymous_island",
69
+ "id",
70
+ "island",
71
+ "location",
72
+ "name",
73
+ "owner",
74
+ "updated_at"
75
+ ]
65
76
  )
66
77
  end
67
78
  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.11.3
4
+ version: 9.12.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sandro Munda
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-04-09 00:00:00.000000000 Z
11
+ date: 2025-05-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -317,6 +317,7 @@ files:
317
317
  - config/routes.rb
318
318
  - config/routes/actions.rb
319
319
  - lib/forest_liana.rb
320
+ - lib/forest_liana/active_record_override.rb
320
321
  - lib/forest_liana/base64_string_io.rb
321
322
  - lib/forest_liana/bootstrapper.rb
322
323
  - lib/forest_liana/collection.rb