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 +4 -4
- data/app/services/forest_liana/resources_getter.rb +54 -1
- data/lib/forest_liana/active_record_override.rb +65 -0
- data/lib/forest_liana/engine.rb +7 -0
- data/lib/forest_liana/version.rb +1 -1
- data/spec/dummy/app/models/tree.rb +2 -0
- data/spec/helpers/forest_liana/query_helper_spec.rb +9 -4
- data/spec/requests/resources_spec.rb +30 -4
- data/spec/services/forest_liana/resources_getter_spec.rb +20 -2
- data/spec/services/forest_liana/schema_adapter_spec.rb +12 -1
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a205f59494a1a151c8bb1a5be912905bf7f1c9242136ae63de22f0372f7268bf
|
4
|
+
data.tar.gz: ddba735dde964ffe46c0dc2907edd065e707a3d63d8283f25b2155c903d3eeb5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
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
|
data/lib/forest_liana/engine.rb
CHANGED
@@ -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
|
data/lib/forest_liana/version.rb
CHANGED
@@ -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(
|
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(
|
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
|
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(
|
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
|
-
|
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
|
-
[
|
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.
|
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-
|
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
|