forest_liana 9.20.0 → 9.20.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: c37d0a61be7a37b80bab933cf4892cef1aceb25a5f8351d56c66dbd4d07e6c4d
4
- data.tar.gz: d857a4ebb09833f6c99afb44d4daf2e06b396689331c6e28359fe78117e78727
3
+ metadata.gz: de38bb4323a6676a9185bee03e2ba490c9b9bc2be0fcb41e213849ef9755c9f2
4
+ data.tar.gz: 54e8bd60a653ecdf126480f2530b15cf52c024d9b69acd3152fd7ffa86f52c0e
5
5
  SHA512:
6
- metadata.gz: d8e83baf30e64e0c1a0fed567e62c7f1ed8b1763924a813967090616cf703e103b5f53609b0579d811bd286fd107aef17fa50ec14d92dac67ed8cc77cfc51b12
7
- data.tar.gz: d2c6655ba0c37f0a917018e32ac3afdc0bbcbc3de9ebb82bce59cf2e4043a53145c7897184322a1c372ea9e51c223cc03c59714d312b728b7466044bd8a575a1
6
+ metadata.gz: c319db67ae5621fd89a52d86e02022b73c4a2872763ec1cc48ef5a43c0f9b47b0ae3fa4927c1d180b60d0af83dfaa6085e788f8e66b91c3213c804a9e5fc56b6
7
+ data.tar.gz: 2d59d2fb65f0b2c05e189e11d25d20dcf9980fe9dc23d4c1f14c96050a7ef67bc7e162731b0fe5f6c0a717557ba9b8b12d98d4481197abc21a69b9d2a1c5b900
@@ -58,7 +58,7 @@ module ForestLiana
58
58
  begin
59
59
  # action.hooks[:change] is a hashmap here
60
60
  # to do the validation, only the hook names are require
61
- change_hooks_name = action.hooks[:change].nil? ? nil : action.hooks[:change].keys
61
+ change_hooks_name = action.hooks && action.hooks[:change] ? action.hooks[:change].keys : nil
62
62
  ForestLiana::SmartActionFieldValidator.validate_smart_action_fields(result[:fields], action.name, change_hooks_name)
63
63
  rescue ForestLiana::Errors::SmartActionInvalidFieldError => invalid_field_error
64
64
  FOREST_LOGGER.warn invalid_field_error.message
@@ -107,10 +107,15 @@ module ForestLiana
107
107
  # Get the smart action hook load context
108
108
  context = get_smart_action_load_ctx(action.fields)
109
109
 
110
- # Call the user-defined load hook.
111
- result = action.hooks[:load].(context)
110
+ if action.hooks && action.hooks[:load].present?
111
+ # Call the user-defined load hook.
112
+ result = action.hooks[:load].(context)
112
113
 
113
- handle_result(result, action)
114
+ handle_result(result, action)
115
+ else
116
+ # No load hook declared: answer the static form (Node agent parity) instead of a 404.
117
+ handle_result(context[:fields], action)
118
+ end
114
119
  end
115
120
  end
116
121
 
@@ -266,6 +266,9 @@ module ForestLiana
266
266
  }
267
267
 
268
268
  collection.fields = collection.fields.reject do |field|
269
+ # NOTICE: A primary key column must survive; a type column is never one.
270
+ next false if field[:is_primary_key]
271
+
269
272
  field[:field] == association.foreign_key || field[:field] == association.foreign_type
270
273
  end
271
274
  # NOTICE: Delete the association if the targeted model is excluded.
@@ -274,7 +277,8 @@ module ForestLiana
274
277
  x[:field] == association.foreign_key
275
278
  end
276
279
 
277
- collection.fields.delete(field) if field
280
+ # NOTICE: A primary key column must survive, even as an excluded association's FK.
281
+ collection.fields.delete(field) if field && !field[:is_primary_key]
278
282
  # NOTICE: The foreign key exists, so it's a belongsTo relationship.
279
283
  elsif (field = column_association(collection, association)) &&
280
284
  [:has_one, :belongs_to].include?(association.macro)
@@ -1,9 +1,8 @@
1
1
  ForestLiana.apimap.each do |collection|
2
2
  if !collection.actions.empty?
3
3
  collection.actions.each do |action|
4
- if action.hooks && action.hooks[:load].present?
5
- post action.endpoint.sub('forest', '') + '/hooks/load' => 'actions#load', action_name: ActiveSupport::Inflector.parameterize(action.name)
6
- end
4
+ # Unconditional: clients probe /hooks/load even when no load hook is declared.
5
+ post action.endpoint.sub('forest', '') + '/hooks/load' => 'actions#load', action_name: ActiveSupport::Inflector.parameterize(action.name)
7
6
  if action.hooks && action.hooks[:change].present?
8
7
  post action.endpoint.sub('forest', '') + '/hooks/change' => 'actions#change', action_name: ActiveSupport::Inflector.parameterize(action.name)
9
8
  end
@@ -1,3 +1,3 @@
1
1
  module ForestLiana
2
- VERSION = "9.20.0"
2
+ VERSION = "9.20.2"
3
3
  end
@@ -28,6 +28,19 @@ class Forest::Island
28
28
 
29
29
  action 'test'
30
30
 
31
+ action 'static_form_action',
32
+ fields: [{
33
+ field: 'static_field',
34
+ type: 'String',
35
+ default_value: nil,
36
+ enums: nil,
37
+ is_required: false,
38
+ is_read_only: false,
39
+ reference: nil,
40
+ description: nil,
41
+ widget: nil
42
+ }]
43
+
31
44
  action 'my_action',
32
45
  fields: [foo],
33
46
  hooks: {
@@ -130,6 +130,25 @@ describe 'Requesting Actions routes', :type => :request do
130
130
  expect(response.status).to eq(422)
131
131
  end
132
132
 
133
+ it 'should respond 200 with the static form when the action has no load hook' do
134
+ post '/forest/actions/static_form_action/hooks/load', params: JSON.dump(params), headers: headers
135
+ action = island.actions.select { |action| action.name == 'static_form_action' }.first
136
+ static_field = action.fields.select { |field| field[:field] == 'static_field' }.first
137
+ expect(response.status).to eq(200)
138
+ expect(JSON.parse(response.body)).to eq({'fields' => [static_field.merge({:value => nil}).transform_keys { |key| key.to_s.camelize(:lower) }.stringify_keys]})
139
+
140
+ first_body = response.body
141
+ post '/forest/actions/static_form_action/hooks/load', params: JSON.dump(params), headers: headers
142
+ expect(response.status).to eq(200)
143
+ expect(response.body).to eq(first_body)
144
+ end
145
+
146
+ it 'should respond 200 with empty fields when the action has no form at all' do
147
+ post '/forest/actions/test/hooks/load', params: JSON.dump(params), headers: headers
148
+ expect(response.status).to eq(200)
149
+ expect(JSON.parse(response.body)).to eq({'fields' => []})
150
+ end
151
+
133
152
  it 'should respond 500 with bad hook result type' do
134
153
  post '/forest/actions/fail_action/hooks/load', params: JSON.dump(params), headers: headers
135
154
  expect(response.status).to eq(500)
@@ -1,4 +1,7 @@
1
1
  require 'rails_helper'
2
+ require 'zlib'
3
+ require 'stringio'
4
+ require 'socket'
2
5
 
3
6
  describe 'Workflow executor proxy', type: :request do
4
7
  let(:run_id) { 'run_abc123' }
@@ -207,6 +210,53 @@ describe 'Workflow executor proxy', type: :request do
207
210
  end
208
211
  end
209
212
 
213
+ # Real local server + real HTTParty (no stub) to exercise actual gzip decompression.
214
+ describe 'when the executor gzip-compresses the response (nginx in front)' do
215
+ let(:gz_body) do
216
+ io = StringIO.new
217
+ writer = Zlib::GzipWriter.new(io)
218
+ writer.write({ 'id' => run_id, 'state' => 'pending' }.to_json)
219
+ writer.close
220
+ io.string
221
+ end
222
+
223
+ around do |example|
224
+ server = TCPServer.new('127.0.0.1', 0)
225
+ port = server.addr[1]
226
+ thread = Thread.new do
227
+ client = server.accept
228
+ client.readpartial(4096)
229
+ client.write(
230
+ "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\n" \
231
+ "Content-Encoding: gzip\r\nContent-Length: #{gz_body.bytesize}\r\n" \
232
+ "Connection: close\r\n\r\n"
233
+ )
234
+ client.write(gz_body)
235
+ client.close
236
+ rescue IOError, Errno::ECONNRESET, Errno::EPIPE
237
+ nil
238
+ end
239
+
240
+ original = ForestLiana.workflow_executor_url
241
+ ForestLiana.workflow_executor_url = "http://127.0.0.1:#{port}"
242
+ example.run
243
+ ensure
244
+ ForestLiana.workflow_executor_url = original
245
+ thread&.kill
246
+ server&.close
247
+ end
248
+
249
+ before { allow(HTTParty).to receive(:get).and_call_original }
250
+
251
+ it 'decompresses the body and drops the stale content-encoding' do
252
+ get "/forest/_internal/executor/runs/#{run_id}", headers: auth_headers
253
+
254
+ expect(response.status).to eq(200)
255
+ expect(JSON.parse(response.body)).to eq('id' => run_id, 'state' => 'pending')
256
+ expect(response.headers['content-encoding']).to be_nil
257
+ end
258
+ end
259
+
210
260
  describe 'when ForestLiana.workflow_executor_url is blank' do
211
261
  around do |example|
212
262
  original = ForestLiana.workflow_executor_url
@@ -86,6 +86,62 @@ module ForestLiana
86
86
  ForestLiana.apimap << original if original
87
87
  end
88
88
 
89
+ test 'a primary key column that is also the foreign key of an excluded association is kept' do
90
+ excluded_models = ForestLiana.excluded_models
91
+ name = ForestLiana.name_for(BelongsToField)
92
+ original = ForestLiana.apimap.find { |c| c.name.to_s == name }
93
+ ForestLiana.apimap.delete(original) if original
94
+ ForestLiana.excluded_models = [ForestLiana.name_for(HasOneField)]
95
+ BelongsToField.define_singleton_method(:primary_key) { 'has_one_field_id' }
96
+
97
+ schema = SchemaAdapter.new(BelongsToField).perform
98
+ field = schema.fields.find { |f| f[:field] == 'has_one_field_id' }
99
+
100
+ refute_nil field, 'the primary key column must not be deleted'
101
+ assert_equal true, field[:is_primary_key]
102
+ ensure
103
+ BelongsToField.singleton_class.send(:remove_method, :primary_key)
104
+ ForestLiana.excluded_models = excluded_models
105
+ rebuilt = ForestLiana.apimap.find { |c| c.name.to_s == name }
106
+ ForestLiana.apimap.delete(rebuilt) if rebuilt
107
+ ForestLiana.apimap << original if original
108
+ end
109
+
110
+ test 'an ordinary foreign key of an excluded association is still deleted' do
111
+ excluded_models = ForestLiana.excluded_models
112
+ name = ForestLiana.name_for(BelongsToField)
113
+ original = ForestLiana.apimap.find { |c| c.name.to_s == name }
114
+ ForestLiana.apimap.delete(original) if original
115
+ ForestLiana.excluded_models = [ForestLiana.name_for(HasOneField)]
116
+
117
+ schema = SchemaAdapter.new(BelongsToField).perform
118
+
119
+ assert_nil schema.fields.find { |f| f[:field] == 'has_one_field_id' }
120
+ ensure
121
+ ForestLiana.excluded_models = excluded_models
122
+ rebuilt = ForestLiana.apimap.find { |c| c.name.to_s == name }
123
+ ForestLiana.apimap.delete(rebuilt) if rebuilt
124
+ ForestLiana.apimap << original if original
125
+ end
126
+
127
+ test 'a primary key column that is also a polymorphic foreign key is kept' do
128
+ name = ForestLiana.name_for(PolymorphicField)
129
+ original = ForestLiana.apimap.find { |c| c.name.to_s == name }
130
+ ForestLiana.apimap.delete(original) if original
131
+ PolymorphicField.define_singleton_method(:primary_key) { 'has_one_field_id' }
132
+
133
+ schema = SchemaAdapter.new(PolymorphicField).perform
134
+ field = schema.fields.find { |f| f[:field] == 'has_one_field_id' }
135
+
136
+ refute_nil field, 'the primary key column must not be rejected'
137
+ assert_equal true, field[:is_primary_key]
138
+ ensure
139
+ PolymorphicField.singleton_class.send(:remove_method, :primary_key)
140
+ rebuilt = ForestLiana.apimap.find { |c| c.name.to_s == name }
141
+ ForestLiana.apimap.delete(rebuilt) if rebuilt
142
+ ForestLiana.apimap << original if original
143
+ end
144
+
89
145
  test 'belongsTo relationship' do
90
146
  schema = SchemaAdapter.new(BelongsToField).perform
91
147
  fields = schema.fields.select do |field|
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.20.0
4
+ version: 9.20.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-06-26 00:00:00.000000000 Z
11
+ date: 2026-07-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails