forest_liana 9.17.0 → 9.17.2

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: 79bda2b54d1daacca266bd2f1b8197ef59a8964c46d895fda1c240bdbcc6b7d1
4
- data.tar.gz: 1f60f2be8749c3d3e3aeb9208e4cae70cd3616030a6563980ca843743186073c
3
+ metadata.gz: 44ec79f4e42ec91b8c5306d87672aab03a6b30072e88e27fdcb311d3d7d2c8de
4
+ data.tar.gz: e5f37934ccdbf40a78499fd22fcafcd037b8eed1b12034c773bbb499d16b60c7
5
5
  SHA512:
6
- metadata.gz: 9a3754ed9509e127884404b37c1afb9a059522379673b48514c511dc5f94bb97c0dfef68bc2d7284dae25dea580875a772bae8eb464d529ab79c1eb96315c459
7
- data.tar.gz: 89c13361f05f461155aae07c8f63b2524513ff50342506d9e7139ea5c9dcbea7fcaf67166a27f6601199696861d4acc611d7d4461e0aa6c4f75998b2ea9bd0a4
6
+ metadata.gz: 93d789834be64fe90cdc9b58c8201df5b75396d627a1138ec1d46e437cfb60d7d63246ece332db3890a6a0cb5eb672683655abc5736dc5c85d38ab2aea47e41a
7
+ data.tar.gz: 91b3c9bf5856f4b628ca31b4140bab3f00b96bd3a7afe670cefaa8701499a86e2fca9e0c431b7a621c86283bae0eefa13afb5104f25ad89589105eb007fa34b9
@@ -1,5 +1,7 @@
1
1
  module ForestLiana
2
2
  class BaseGetter
3
+ include ForestLiana::RecordFindable
4
+
3
5
  def get_collection(collection_name)
4
6
  ForestLiana.apimap.find { |collection| collection.name.to_s == collection_name }
5
7
  end
@@ -1,5 +1,7 @@
1
1
  module ForestLiana
2
2
  class BelongsToUpdater
3
+ include ForestLiana::RecordFindable
4
+
3
5
  attr_accessor :errors
4
6
 
5
7
  def initialize(resource, association, params)
@@ -12,7 +14,7 @@ module ForestLiana
12
14
 
13
15
  def perform
14
16
  begin
15
- @record = @resource.find(@params[:id])
17
+ @record = find_record(@resource, @resource, @params[:id])
16
18
  if (SchemaUtils.polymorphic?(@association))
17
19
  if @data.nil?
18
20
  new_value = nil
@@ -1,5 +1,7 @@
1
1
  module ForestLiana
2
2
  class HasManyAssociator
3
+ include ForestLiana::RecordFindable
4
+
3
5
  def initialize(resource, association, params)
4
6
  @resource = resource
5
7
  @association = association
@@ -8,8 +10,8 @@ module ForestLiana
8
10
  end
9
11
 
10
12
  def perform
11
- @record = @resource.find(@params[:id])
12
- associated_records = @resource.find(@params[:id]).send(@association.name)
13
+ @record = find_record(@resource, @resource, @params[:id])
14
+ associated_records = @record.send(@association.name)
13
15
 
14
16
  if @data.is_a?(Array)
15
17
  @data.each do |record_added|
@@ -1,5 +1,7 @@
1
1
  module ForestLiana
2
2
  class HasManyDissociator
3
+ include ForestLiana::RecordFindable
4
+
3
5
  def initialize(resource, association, params, forest_user)
4
6
  @resource = resource
5
7
  @association = association
@@ -10,8 +12,8 @@ module ForestLiana
10
12
  end
11
13
 
12
14
  def perform
13
- @record = @resource.find(@params[:id])
14
- associated_records = @resource.find(@params[:id]).send(@association.name)
15
+ @record = find_record(@resource, @resource, @params[:id])
16
+ associated_records = @record.send(@association.name)
15
17
 
16
18
  remove_association = !@with_deletion || @association.macro == :has_and_belongs_to_many
17
19
  if @data.is_a?(Array)
@@ -29,6 +29,9 @@ module ForestLiana
29
29
 
30
30
  if association_class.primary_key.is_a?(Array)
31
31
  adapter_name = association_class.connection.adapter_name.downcase
32
+ pk_columns = association_class.primary_key.map do |pk|
33
+ "#{association_class.table_name}.#{pk}"
34
+ end.join(', ')
32
35
 
33
36
  if adapter_name.include?('sqlite')
34
37
  # For SQLite: concatenate columns for DISTINCT count
@@ -37,12 +40,9 @@ module ForestLiana
37
40
  end.join(" || '|' || ")
38
41
 
39
42
  @records_count = @records.distinct.count(Arel.sql(pk_concat))
43
+ elsif adapter_name.include?('postgresql')
44
+ @records_count = @records.distinct.count(Arel.sql("ROW(#{pk_columns})"))
40
45
  else
41
- # For PostgreSQL/MySQL: use DISTINCT with multiple columns
42
- pk_columns = association_class.primary_key.map do |pk|
43
- "#{association_class.table_name}.#{pk}"
44
- end.join(', ')
45
-
46
46
  @records_count = @records.distinct.count(Arel.sql(pk_columns))
47
47
  end
48
48
  else
@@ -94,7 +94,7 @@ module ForestLiana
94
94
  end
95
95
 
96
96
  def prepare_query
97
- parent_record = ForestLiana::Utils::CompositePrimaryKeyHelper.find_record(get_resource(), @resource, @params[:id])
97
+ parent_record = find_record(get_resource(), @resource, @params[:id])
98
98
  association = parent_record.send(@params[:association_name])
99
99
  @records = optimize_record_loading(association, @search_query_builder.perform(association))
100
100
  end
@@ -0,0 +1,27 @@
1
+ module ForestLiana
2
+ module RecordFindable
3
+ private
4
+
5
+ def find_record(scope, resource, id)
6
+ primary_key = resource.primary_key
7
+
8
+ if primary_key.is_a?(Array)
9
+ id_values = parse_composite_id(id)
10
+ conditions = primary_key.zip(id_values).to_h
11
+ scope.find_by(conditions)
12
+ else
13
+ scope.find(id)
14
+ end
15
+ end
16
+
17
+ def parse_composite_id(id)
18
+ return id if id.is_a?(Array)
19
+
20
+ if id.to_s.start_with?('[') && id.to_s.end_with?(']')
21
+ JSON.parse(id.to_s)
22
+ else
23
+ raise ForestLiana::Errors::HTTP422Error.new("Composite primary key ID must be in format [value1,value2], received: #{id}")
24
+ end
25
+ end
26
+ end
27
+ end
@@ -14,7 +14,7 @@ module ForestLiana
14
14
  def perform
15
15
  records = optimize_record_loading(@resource, get_resource())
16
16
  scoped_records = ForestLiana::ScopeManager.apply_scopes_on_records(records, @user, @collection_name, @params[:timezone])
17
- @record = ForestLiana::Utils::CompositePrimaryKeyHelper.find_record(scoped_records, @resource, @params[:id])
17
+ @record = find_record(scoped_records, @resource, @params[:id])
18
18
  end
19
19
  end
20
20
  end
@@ -1,5 +1,7 @@
1
1
  module ForestLiana
2
2
  class ResourceUpdater
3
+ include ForestLiana::RecordFindable
4
+
3
5
  attr_accessor :record
4
6
  attr_accessor :errors
5
7
 
@@ -14,7 +16,7 @@ module ForestLiana
14
16
  begin
15
17
  collection_name = ForestLiana.name_for(@resource)
16
18
  scoped_records = ForestLiana::ScopeManager.apply_scopes_on_records(@resource, @user, collection_name, @params[:timezone])
17
- @record = scoped_records.find(@params[:id])
19
+ @record = find_record(scoped_records, @resource, @params[:id])
18
20
 
19
21
  if has_strong_parameter
20
22
  @record.update(resource_params)
@@ -454,7 +454,31 @@ module ForestLiana
454
454
  def add_default_value(column_schema, column)
455
455
  # TODO: detect/introspect the attribute default value with Rails 5
456
456
  # ex: attribute :email, :string, default: 'arnaud@forestadmin.com'
457
- column_schema[:default_value] = column.default if column.default
457
+
458
+ unless column.default.nil?
459
+ column_schema[:default_value] = normalize_default_value(column)
460
+ end
461
+ end
462
+
463
+ def normalize_default_value(column)
464
+ # In Ruby 4.0+ and Rails 8+, ActiveRecord returns default values as strings
465
+ case column.type
466
+ when :boolean
467
+ case column.default.to_s
468
+ when '0', 'f', 'false'
469
+ false
470
+ when '1', 't', 'true'
471
+ true
472
+ else
473
+ column.default
474
+ end
475
+ when :integer
476
+ column.default.to_i
477
+ when :float, :decimal
478
+ column.default.to_f
479
+ else
480
+ column.default
481
+ end
458
482
  end
459
483
 
460
484
  def add_validations(column_schema, column)
@@ -1,3 +1,3 @@
1
1
  module ForestLiana
2
- VERSION = "9.17.0"
2
+ VERSION = "9.17.2"
3
3
  end
@@ -0,0 +1,223 @@
1
+ require 'rails_helper'
2
+
3
+ module ForestLiana
4
+ describe 'Composite Primary Keys Support' do
5
+ let(:rendering_id) { 13 }
6
+ let(:user) { { 'id' => '1', 'rendering_id' => rendering_id } }
7
+ let(:scopes) { { 'scopes' => {}, 'team' => { 'id' => '1', 'name' => 'Operations' } } }
8
+
9
+ before do
10
+ ForestLiana::ScopeManager.invalidate_scope_cache(rendering_id)
11
+ allow(ForestLiana::ScopeManager).to receive(:fetch_scopes).and_return(scopes)
12
+ end
13
+
14
+ describe RecordFindable do
15
+ # Create a test class that includes the module to test private methods
16
+ let(:test_class) do
17
+ Class.new do
18
+ include ForestLiana::RecordFindable
19
+ # Expose private methods for testing
20
+ public :find_record, :parse_composite_id
21
+ end
22
+ end
23
+
24
+ let(:helper) { test_class.new }
25
+
26
+ describe '#parse_composite_id' do
27
+ it 'correctly parses composite ID in JSON format' do
28
+ expect(helper.parse_composite_id('[1,2]')).to eq([1, 2])
29
+ expect(helper.parse_composite_id('[10,20]')).to eq([10, 20])
30
+ expect(helper.parse_composite_id('["a","b"]')).to eq(['a', 'b'])
31
+ end
32
+
33
+ it 'returns array as-is when already an array' do
34
+ expect(helper.parse_composite_id([1, 2])).to eq([1, 2])
35
+ end
36
+
37
+ it 'raises error for invalid composite ID format' do
38
+ expect {
39
+ helper.parse_composite_id('invalid')
40
+ }.to raise_error(ForestLiana::Errors::HTTP422Error)
41
+ end
42
+ end
43
+
44
+ describe '#find_record' do
45
+ it 'finds record using composite key conditions' do
46
+ mock_resource = double('Resource', primary_key: [:user_id, :island_id])
47
+ mock_scoped_records = double('ScopedRecords')
48
+ mock_record = double('Record')
49
+
50
+ allow(mock_scoped_records).to receive(:find_by)
51
+ .with({ user_id: 1, island_id: 2 })
52
+ .and_return(mock_record)
53
+
54
+ result = helper.find_record(mock_scoped_records, mock_resource, '[1,2]')
55
+
56
+ expect(result).to eq(mock_record)
57
+ expect(mock_scoped_records).to have_received(:find_by).with({ user_id: 1, island_id: 2 })
58
+ end
59
+
60
+ it 'falls back to standard find for simple primary key' do
61
+ mock_resource = double('Resource', primary_key: 'id')
62
+ mock_scoped_records = double('ScopedRecords')
63
+ mock_record = double('Record')
64
+
65
+ allow(mock_scoped_records).to receive(:find).with(123).and_return(mock_record)
66
+
67
+ result = helper.find_record(mock_scoped_records, mock_resource, 123)
68
+
69
+ expect(result).to eq(mock_record)
70
+ expect(mock_scoped_records).to have_received(:find).with(123)
71
+ end
72
+ end
73
+ end
74
+
75
+ describe HasManyGetter do
76
+ describe '#count with composite primary key' do
77
+ let(:composite_association_class) do
78
+ mock_class = double('CompositeModel')
79
+ allow(mock_class).to receive(:primary_key).and_return([:user_id, :island_id])
80
+ allow(mock_class).to receive(:table_name).and_return('user_islands')
81
+ allow(mock_class).to receive(:connection).and_return(double('Connection', adapter_name: adapter_name))
82
+ mock_class
83
+ end
84
+
85
+ let(:mock_records) { double('Records') }
86
+
87
+ before do
88
+ allow(mock_records).to receive(:distinct).and_return(mock_records)
89
+ allow(mock_records).to receive(:count).and_return(5)
90
+ end
91
+
92
+ context 'with PostgreSQL adapter' do
93
+ let(:adapter_name) { 'PostgreSQL' }
94
+
95
+ it 'uses ROW() syntax for COUNT DISTINCT' do
96
+ getter = HasManyGetter.allocate
97
+ getter.instance_variable_set(:@records, mock_records)
98
+
99
+ allow(getter).to receive(:model_association).and_return(composite_association_class)
100
+
101
+ getter.count
102
+
103
+ expect(mock_records).to have_received(:count) do |arg|
104
+ expect(arg.to_s).to include('ROW(')
105
+ expect(arg.to_s).to include('user_islands.user_id')
106
+ expect(arg.to_s).to include('user_islands.island_id')
107
+ end
108
+ end
109
+ end
110
+
111
+ context 'with MySQL adapter' do
112
+ let(:adapter_name) { 'Mysql2' }
113
+
114
+ it 'uses standard multi-column syntax for COUNT DISTINCT' do
115
+ getter = HasManyGetter.allocate
116
+ getter.instance_variable_set(:@records, mock_records)
117
+
118
+ allow(getter).to receive(:model_association).and_return(composite_association_class)
119
+
120
+ getter.count
121
+
122
+ expect(mock_records).to have_received(:count) do |arg|
123
+ expect(arg.to_s).not_to include('ROW(')
124
+ expect(arg.to_s).to include('user_islands.user_id, user_islands.island_id')
125
+ end
126
+ end
127
+ end
128
+
129
+ context 'with SQLite adapter' do
130
+ let(:adapter_name) { 'SQLite' }
131
+
132
+ it 'uses concatenation syntax for COUNT DISTINCT' do
133
+ getter = HasManyGetter.allocate
134
+ getter.instance_variable_set(:@records, mock_records)
135
+
136
+ allow(getter).to receive(:model_association).and_return(composite_association_class)
137
+
138
+ getter.count
139
+
140
+ expect(mock_records).to have_received(:count) do |arg|
141
+ expect(arg.to_s).to include("||")
142
+ expect(arg.to_s).to include("'|'")
143
+ end
144
+ end
145
+ end
146
+ end
147
+ end
148
+
149
+ describe BelongsToUpdater do
150
+ describe 'with composite primary key parent' do
151
+ it 'uses find_record to find the parent record' do
152
+ composite_model = double('CompositeModel')
153
+ association = double('Association', name: :user, klass: User)
154
+ mock_record = double('Record', save: true)
155
+ allow(mock_record).to receive(:user=)
156
+
157
+ params = { id: '[1,2]', 'data' => { id: '5', type: 'User' } }
158
+
159
+ updater = described_class.new(composite_model, association, params)
160
+ allow(updater).to receive(:find_record).and_return(mock_record)
161
+ allow(User).to receive(:find).and_return(double('User'))
162
+ allow(SchemaUtils).to receive(:polymorphic?).and_return(false)
163
+
164
+ updater.perform
165
+
166
+ expect(updater).to have_received(:find_record)
167
+ .with(composite_model, composite_model, '[1,2]')
168
+ end
169
+ end
170
+ end
171
+
172
+ describe HasManyAssociator do
173
+ describe 'with composite primary key parent' do
174
+ it 'uses find_record to find the parent record' do
175
+ composite_model = double('CompositeModel')
176
+ association = double('Association', name: :trees, klass: Tree)
177
+ mock_record = double('Record')
178
+ mock_associated_records = double('AssociatedRecords')
179
+
180
+ allow(mock_record).to receive(:send).with(:trees).and_return(mock_associated_records)
181
+ allow(mock_associated_records).to receive(:<<)
182
+
183
+ params = { id: '[1,2]', 'data' => [{ id: '5' }] }
184
+
185
+ associator = described_class.new(composite_model, association, params)
186
+ allow(associator).to receive(:find_record).and_return(mock_record)
187
+ allow(Tree).to receive(:find).and_return(double('Tree'))
188
+
189
+ associator.perform
190
+
191
+ expect(associator).to have_received(:find_record)
192
+ .with(composite_model, composite_model, '[1,2]')
193
+ end
194
+ end
195
+ end
196
+
197
+ describe HasManyDissociator do
198
+ describe 'with composite primary key parent' do
199
+ it 'uses find_record to find the parent record' do
200
+ composite_model = double('CompositeModel')
201
+ association = double('Association', name: :trees, klass: Tree, macro: :has_many)
202
+ mock_record = double('Record')
203
+ mock_associated_records = double('AssociatedRecords')
204
+
205
+ allow(mock_record).to receive(:send).with(:trees).and_return(mock_associated_records)
206
+ allow(mock_associated_records).to receive(:delete)
207
+
208
+ params = { id: '[1,2]', 'data' => [{ id: '5' }], delete: 'false' }
209
+ forest_user = user
210
+
211
+ dissociator = described_class.new(composite_model, association, params, forest_user)
212
+ allow(dissociator).to receive(:find_record).and_return(mock_record)
213
+ allow(Tree).to receive(:find).and_return(double('Tree'))
214
+
215
+ dissociator.perform
216
+
217
+ expect(dissociator).to have_received(:find_record)
218
+ .with(composite_model, composite_model, '[1,2]')
219
+ end
220
+ end
221
+ end
222
+ end
223
+ end
@@ -0,0 +1,157 @@
1
+ module ForestLiana
2
+ describe SchemaAdapter do
3
+ describe 'normalize_default_value' do
4
+ context 'with boolean fields' do
5
+ before(:each) do
6
+ @original_apimap = ForestLiana.apimap.dup
7
+ @original_models = ForestLiana.models.dup
8
+
9
+ Object.const_set(:BooleanFieldWithDefault, Class.new(ActiveRecord::Base) do
10
+ self.table_name = 'boolean_fields_with_defaults'
11
+ end)
12
+
13
+ ActiveRecord::Migration.suppress_messages do
14
+ if ActiveRecord::Base.connection.table_exists?('boolean_fields_with_defaults')
15
+ ActiveRecord::Base.connection.drop_table('boolean_fields_with_defaults')
16
+ end
17
+ ActiveRecord::Base.connection.create_table('boolean_fields_with_defaults') do |t|
18
+ t.boolean :active, default: false
19
+ t.boolean :verified, default: true
20
+ t.boolean :nullable
21
+ end
22
+ end
23
+ end
24
+
25
+ after(:each) do
26
+ ForestLiana.apimap = @original_apimap
27
+ ForestLiana.models = @original_models
28
+
29
+ ActiveRecord::Migration.suppress_messages do
30
+ ActiveRecord::Base.connection.drop_table('boolean_fields_with_defaults') if ActiveRecord::Base.connection.table_exists?('boolean_fields_with_defaults')
31
+ end
32
+ Object.send(:remove_const, :BooleanFieldWithDefault) if Object.const_defined?(:BooleanFieldWithDefault)
33
+ end
34
+
35
+ it 'should convert boolean default values to proper booleans' do
36
+ ForestLiana.models = [BooleanFieldWithDefault]
37
+ ForestLiana.apimap = []
38
+
39
+ adapter = SchemaAdapter.new(BooleanFieldWithDefault)
40
+ collection = adapter.perform
41
+
42
+ active_field = collection.fields.find { |f| f[:field] == 'active' }
43
+ verified_field = collection.fields.find { |f| f[:field] == 'verified' }
44
+ nullable_field = collection.fields.find { |f| f[:field] == 'nullable' }
45
+
46
+ expect(active_field[:default_value]).to eq(false)
47
+ expect(active_field[:default_value].class).to eq(FalseClass)
48
+
49
+ expect(verified_field[:default_value]).to eq(true)
50
+ expect(verified_field[:default_value].class).to eq(TrueClass)
51
+
52
+ expect(nullable_field[:default_value]).to be_nil
53
+ end
54
+ end
55
+
56
+ context 'with enum fields' do
57
+ before(:each) do
58
+ @original_apimap = ForestLiana.apimap.dup
59
+ @original_models = ForestLiana.models.dup
60
+
61
+ Object.const_set(:EnumFieldModel, Class.new(ActiveRecord::Base) do
62
+ self.table_name = 'enum_field_models'
63
+ enum status: { inactive: 0, active: 1, archived: 2 }
64
+ enum role: { user: "0", admin: "1", superadmin: "2" }
65
+ end)
66
+
67
+ ActiveRecord::Migration.suppress_messages do
68
+ if ActiveRecord::Base.connection.table_exists?('enum_field_models')
69
+ ActiveRecord::Base.connection.drop_table('enum_field_models')
70
+ end
71
+ ActiveRecord::Base.connection.create_table('enum_field_models') do |t|
72
+ t.integer :status, default: 0
73
+ t.integer :role, default: 1
74
+ end
75
+ end
76
+ end
77
+
78
+ after(:each) do
79
+ ForestLiana.apimap = @original_apimap
80
+ ForestLiana.models = @original_models
81
+
82
+ ActiveRecord::Migration.suppress_messages do
83
+ ActiveRecord::Base.connection.drop_table('enum_field_models') if ActiveRecord::Base.connection.table_exists?('enum_field_models')
84
+ end
85
+ Object.send(:remove_const, :EnumFieldModel) if Object.const_defined?(:EnumFieldModel)
86
+ end
87
+
88
+ it 'should convert enum default values to integers' do
89
+ ForestLiana.models = [EnumFieldModel]
90
+ ForestLiana.apimap = []
91
+
92
+ adapter = SchemaAdapter.new(EnumFieldModel)
93
+ collection = adapter.perform
94
+
95
+ status_field = collection.fields.find { |f| f[:field] == 'status' }
96
+ role_field = collection.fields.find { |f| f[:field] == 'role' }
97
+
98
+ expect(status_field[:default_value]).to eq(0)
99
+ expect(status_field[:default_value].class).to eq(Integer)
100
+
101
+ expect(role_field[:default_value]).to eq(1)
102
+ expect(role_field[:default_value].class).to eq(Integer)
103
+ end
104
+ end
105
+
106
+ context 'with numeric fields' do
107
+ before(:each) do
108
+ @original_apimap = ForestLiana.apimap.dup
109
+ @original_models = ForestLiana.models.dup
110
+
111
+ Object.const_set(:NumericFieldModel, Class.new(ActiveRecord::Base) do
112
+ self.table_name = 'numeric_field_models'
113
+ end)
114
+
115
+ ActiveRecord::Migration.suppress_messages do
116
+ if ActiveRecord::Base.connection.table_exists?('numeric_field_models')
117
+ ActiveRecord::Base.connection.drop_table('numeric_field_models')
118
+ end
119
+ ActiveRecord::Base.connection.create_table('numeric_field_models') do |t|
120
+ t.integer :count, default: 0
121
+ t.float :rate, default: 0.5
122
+ t.decimal :price, default: 9.99
123
+ end
124
+ end
125
+ end
126
+
127
+ after(:each) do
128
+ ForestLiana.apimap = @original_apimap
129
+ ForestLiana.models = @original_models
130
+
131
+ ActiveRecord::Migration.suppress_messages do
132
+ ActiveRecord::Base.connection.drop_table('numeric_field_models') if ActiveRecord::Base.connection.table_exists?('numeric_field_models')
133
+ end
134
+ Object.send(:remove_const, :NumericFieldModel) if Object.const_defined?(:NumericFieldModel)
135
+ end
136
+
137
+ it 'should convert numeric default values to proper types' do
138
+ ForestLiana.models = [NumericFieldModel]
139
+ ForestLiana.apimap = []
140
+
141
+ adapter = SchemaAdapter.new(NumericFieldModel)
142
+ collection = adapter.perform
143
+
144
+ count_field = collection.fields.find { |f| f[:field] == 'count' }
145
+ rate_field = collection.fields.find { |f| f[:field] == 'rate' }
146
+ price_field = collection.fields.find { |f| f[:field] == 'price' }
147
+
148
+ expect(count_field[:default_value]).to eq(0)
149
+ expect(count_field[:default_value].class).to eq(Integer)
150
+
151
+ expect(rate_field[:default_value]).to be_a(Float)
152
+ expect(price_field[:default_value]).to be_a(Float)
153
+ end
154
+ end
155
+ end
156
+ end
157
+ 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.17.0
4
+ version: 9.17.2
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-01-06 00:00:00.000000000 Z
11
+ date: 2026-01-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -311,6 +311,7 @@ files:
311
311
  - app/services/forest_liana/operator_date_interval_parser.rb
312
312
  - app/services/forest_liana/pie_stat_getter.rb
313
313
  - app/services/forest_liana/query_stat_getter.rb
314
+ - app/services/forest_liana/record_findable.rb
314
315
  - app/services/forest_liana/resource_creator.rb
315
316
  - app/services/forest_liana/resource_getter.rb
316
317
  - app/services/forest_liana/resource_updater.rb
@@ -334,7 +335,6 @@ files:
334
335
  - app/services/forest_liana/stripe_subscriptions_getter.rb
335
336
  - app/services/forest_liana/token.rb
336
337
  - app/services/forest_liana/utils/beta_schema_utils.rb
337
- - app/services/forest_liana/utils/composite_primary_key_helper.rb
338
338
  - app/services/forest_liana/utils/context_variables.rb
339
339
  - app/services/forest_liana/utils/context_variables_injector.rb
340
340
  - app/services/forest_liana/value_stat_getter.rb
@@ -452,6 +452,7 @@ files:
452
452
  - spec/services/forest_liana/ability/permission/smart_action_checker_spec.rb
453
453
  - spec/services/forest_liana/ability/permission_spec.rb
454
454
  - spec/services/forest_liana/apimap_sorter_spec.rb
455
+ - spec/services/forest_liana/composite_primary_keys_spec.rb
455
456
  - spec/services/forest_liana/filters_parser_spec.rb
456
457
  - spec/services/forest_liana/forest_api_requester_spec.rb
457
458
  - spec/services/forest_liana/has_many_getter_spec.rb
@@ -461,6 +462,7 @@ files:
461
462
  - spec/services/forest_liana/resource_updater_spec.rb
462
463
  - spec/services/forest_liana/resources_getter_composite_keys_spec.rb
463
464
  - spec/services/forest_liana/resources_getter_spec.rb
465
+ - spec/services/forest_liana/schema_adapter_default_values_spec.rb
464
466
  - spec/services/forest_liana/schema_adapter_spec.rb
465
467
  - spec/services/forest_liana/scope_manager_spec.rb
466
468
  - spec/services/forest_liana/serializer_factory_spec.rb
@@ -760,6 +762,7 @@ test_files:
760
762
  - spec/services/forest_liana/ability/permission/smart_action_checker_spec.rb
761
763
  - spec/services/forest_liana/ability/permission_spec.rb
762
764
  - spec/services/forest_liana/apimap_sorter_spec.rb
765
+ - spec/services/forest_liana/composite_primary_keys_spec.rb
763
766
  - spec/services/forest_liana/filters_parser_spec.rb
764
767
  - spec/services/forest_liana/forest_api_requester_spec.rb
765
768
  - spec/services/forest_liana/has_many_getter_spec.rb
@@ -769,6 +772,7 @@ test_files:
769
772
  - spec/services/forest_liana/resource_updater_spec.rb
770
773
  - spec/services/forest_liana/resources_getter_composite_keys_spec.rb
771
774
  - spec/services/forest_liana/resources_getter_spec.rb
775
+ - spec/services/forest_liana/schema_adapter_default_values_spec.rb
772
776
  - spec/services/forest_liana/schema_adapter_spec.rb
773
777
  - spec/services/forest_liana/scope_manager_spec.rb
774
778
  - spec/services/forest_liana/serializer_factory_spec.rb
@@ -1,27 +0,0 @@
1
- module ForestLiana
2
- module Utils
3
- module CompositePrimaryKeyHelper
4
- def self.find_record(scoped_records, resource, id)
5
- primary_key = resource.primary_key
6
-
7
- if primary_key.is_a?(Array)
8
- id_values = parse_composite_id(id)
9
- conditions = primary_key.zip(id_values).to_h
10
- scoped_records.find_by(conditions)
11
- else
12
- scoped_records.find(id)
13
- end
14
- end
15
-
16
- def self.parse_composite_id(id)
17
- return id if id.is_a?(Array)
18
-
19
- if id.to_s.start_with?('[') && id.to_s.end_with?(']')
20
- JSON.parse(id.to_s)
21
- else
22
- raise ForestLiana::Errors::HTTP422Error.new("Composite primary key ID must be in format [value1,value2], received: #{id}")
23
- end
24
- end
25
- end
26
- end
27
- end