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 +4 -4
- data/app/services/forest_liana/base_getter.rb +2 -0
- data/app/services/forest_liana/belongs_to_updater.rb +3 -1
- data/app/services/forest_liana/has_many_associator.rb +4 -2
- data/app/services/forest_liana/has_many_dissociator.rb +4 -2
- data/app/services/forest_liana/has_many_getter.rb +6 -6
- data/app/services/forest_liana/record_findable.rb +27 -0
- data/app/services/forest_liana/resource_getter.rb +1 -1
- data/app/services/forest_liana/resource_updater.rb +3 -1
- data/app/services/forest_liana/schema_adapter.rb +25 -1
- data/lib/forest_liana/version.rb +1 -1
- data/spec/services/forest_liana/composite_primary_keys_spec.rb +223 -0
- data/spec/services/forest_liana/schema_adapter_default_values_spec.rb +157 -0
- metadata +7 -3
- data/app/services/forest_liana/utils/composite_primary_key_helper.rb +0 -27
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 44ec79f4e42ec91b8c5306d87672aab03a6b30072e88e27fdcb311d3d7d2c8de
|
|
4
|
+
data.tar.gz: e5f37934ccdbf40a78499fd22fcafcd037b8eed1b12034c773bbb499d16b60c7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 93d789834be64fe90cdc9b58c8201df5b75396d627a1138ec1d46e437cfb60d7d63246ece332db3890a6a0cb5eb672683655abc5736dc5c85d38ab2aea47e41a
|
|
7
|
+
data.tar.gz: 91b3c9bf5856f4b628ca31b4140bab3f00b96bd3a7afe670cefaa8701499a86e2fca9e0c431b7a621c86283bae0eefa13afb5104f25ad89589105eb007fa34b9
|
|
@@ -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
|
|
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
|
|
12
|
-
associated_records = @
|
|
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
|
|
14
|
-
associated_records = @
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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)
|
data/lib/forest_liana/version.rb
CHANGED
|
@@ -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.
|
|
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-
|
|
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
|