forest_admin_rpc_agent 1.13.3 → 1.14.0

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: bc072dfa0eb0ee3eb6030d5940d51a267885e520548c4fb2f2e30e6ff729865d
4
- data.tar.gz: 208ff7e08b008015673153e86eefc713f02f6685e4871d9fc413735cb4a9a8ab
3
+ metadata.gz: 9fb58efd1964b8dbf5c8e9c10b6557297196df287759952fdeff7eef3949e836
4
+ data.tar.gz: 01f76fae8c2ee42d73cd2e454fe9bdaee2a16aea47cf37e38081af45931e884e
5
5
  SHA512:
6
- metadata.gz: bd2154d299d309b5d25b6874b3adabbecf31783f2b9ccbd5046812da27460b42d66261ab8dec2f02121d806c12159fcec46ffef475a549144ccba148d6d39ad8
7
- data.tar.gz: 94d5a94f3277a712cc6e044c3fbff38d9a37e6b281ced666d7fbd32b5f2e4aa51d629635b2ffab77de6cb0a18bc651b2acc0a18c677bf168c667db93ae948f5d
6
+ metadata.gz: 25c49b286bce40a650d73bf684ca6b50b6bd5dfec85d546f5460dfc64dd4babaf01fd67d45f05fea865ab69d94490e53f65626ebc186eb03f3f3c5815680b6de
7
+ data.tar.gz: 3507ba4f38bb3b4344f47958899d797e11a53e8aece6ab3e2cd45b2201630f84b3d7d82bdf4b427cbc87cdf22118dedb9b5bb01ff84d41021211afde3728b36b
data/config/routes.rb CHANGED
@@ -1,14 +1,19 @@
1
1
  ForestAdminRpcAgent::Engine.routes.draw do
2
- route_classes = ForestAdminRpcAgent::Routes.constants.reject { |route| route.to_s == 'BaseRoute' }
2
+ next if defined?(Rake) && Rake.respond_to?(:application) && Rake.application&.top_level_tasks&.any?
3
3
 
4
- route_classes.each do |route|
5
- route_class = ForestAdminRpcAgent::Routes.const_get(route)
6
-
7
- route_instance = route_class.new
8
- if route_instance.respond_to?(:registered)
4
+ begin
5
+ # Use cached_route_instances to avoid recomputing routes during Rails initialization
6
+ ForestAdminRpcAgent::Http::Router.cached_route_instances.each do |route_instance|
9
7
  route_instance.registered(self)
10
- else
11
- Rails.logger.warn "Skipping route: #{route_class} (does not respond to :registered)"
12
8
  end
9
+ rescue StandardError => e
10
+ error_message = "[ForestAdminRpcAgent] CRITICAL: Failed to initialize routes: #{e.class} - #{e.message}\n" \
11
+ "#{e.backtrace.join("\n")}"
12
+ begin
13
+ ForestAdminRpcAgent::Facades::Container.logger.log('Error', error_message)
14
+ rescue StandardError
15
+ puts error_message
16
+ end
17
+ raise e
13
18
  end
14
19
  end
@@ -0,0 +1,92 @@
1
+ module ForestAdminRpcAgent
2
+ module Http
3
+ class Router
4
+ # Mutex for thread-safe cache operations
5
+ @mutex = Mutex.new
6
+
7
+ def self.cached_route_instances
8
+ return route_instances.freeze if cache_disabled?
9
+
10
+ return @cached_route_instances if @cached_route_instances
11
+
12
+ @mutex.synchronize do
13
+ @cached_route_instances ||= begin
14
+ start_time = Time.now
15
+ computed_routes = route_instances
16
+ elapsed = ((Time.now - start_time) * 1000).round(2)
17
+
18
+ log_message = "[ForestAdminRpcAgent] Computed #{computed_routes.size} routes " \
19
+ "in #{elapsed}ms (caching enabled)"
20
+ log_to_available_logger('Info', log_message)
21
+
22
+ computed_routes.freeze
23
+ end
24
+ end
25
+ end
26
+
27
+ def self.cache_disabled?
28
+ config = ForestAdminRpcAgent::Facades::Container.config_from_cache
29
+ config&.dig(:disable_route_cache) == true
30
+ rescue StandardError
31
+ # Config not available, default to caching enabled
32
+ false
33
+ end
34
+
35
+ def self.reset_cached_route_instances!
36
+ @mutex.synchronize do
37
+ @cached_route_instances = nil
38
+ end
39
+ end
40
+
41
+ def self.log_to_available_logger(level, message)
42
+ ForestAdminRpcAgent::Facades::Container.logger.log(level, message)
43
+ rescue StandardError
44
+ puts message
45
+ end
46
+
47
+ def self.route_instances
48
+ route_classes = ForestAdminRpcAgent::Routes.constants.reject { |route| route.to_s == 'BaseRoute' }
49
+
50
+ route_instances = []
51
+
52
+ route_classes.each do |route_name|
53
+ route_class = ForestAdminRpcAgent::Routes.const_get(route_name)
54
+
55
+ # Skip if it's not a class or if it doesn't look like a route
56
+ unless route_class.is_a?(Class)
57
+ log_to_available_logger(
58
+ 'Warn',
59
+ "Skipping constant: #{route_name} (not a class)"
60
+ )
61
+ next
62
+ end
63
+
64
+ begin
65
+ route_instance = route_class.new
66
+
67
+ unless route_instance.respond_to?(:registered)
68
+ log_to_available_logger(
69
+ 'Warn',
70
+ "Skipping route: #{route_class} (does not respond to :registered)"
71
+ )
72
+ next
73
+ end
74
+
75
+ route_instances << route_instance
76
+ rescue ArgumentError => e
77
+ # Skip classes that require constructor arguments (not routes)
78
+ log_to_available_logger(
79
+ 'Warn',
80
+ "Skipping constant: #{route_name} (requires constructor arguments: #{e.message})"
81
+ )
82
+ next
83
+ rescue StandardError => e
84
+ raise e.class, "Failed to instantiate route '#{route_name}': #{e.message}"
85
+ end
86
+ end
87
+
88
+ route_instances
89
+ end
90
+ end
91
+ end
92
+ end
@@ -1,3 +1,5 @@
1
+ require 'json'
2
+
1
3
  module ForestAdminRpcAgent
2
4
  module Middleware
3
5
  class Authentication
@@ -19,7 +21,16 @@ module ForestAdminRpcAgent
19
21
  return [401, { 'Content-Type' => 'application/json' }, [{ error: 'Unauthorized' }.to_json]]
20
22
  end
21
23
 
22
- @app.call(env)
24
+ status, headers, response = @app.call(env)
25
+
26
+ if request.get_header('HTTP_FOREST_CALLER')
27
+ caller = ForestAdminDatasourceToolkit::Components::Caller.new(
28
+ **(JSON.parse(request.get_header('HTTP_FOREST_CALLER')).symbolize_keys)
29
+ )
30
+ headers = headers.merge({ caller: caller })
31
+ end
32
+
33
+ [status, headers, response]
23
34
  end
24
35
 
25
36
  private
@@ -12,19 +12,15 @@ module ForestAdminRpcAgent
12
12
  end
13
13
 
14
14
  def handle_request(args)
15
- return '{}' unless args[:params]['collection_name']
15
+ return {} unless args[:params]['collection_name']
16
16
 
17
17
  datasource = ForestAdminRpcAgent::Facades::Container.datasource
18
18
  collection = datasource.get_collection(args[:params]['collection_name'])
19
-
20
- caller = ForestAdminDatasourceToolkit::Components::Caller.new(
21
- **args[:params]['caller'].to_h.transform_keys(&:to_sym)
22
- )
23
19
  filter = FilterFactory.from_plain_object(args[:params]['filter'])
24
20
  data = args[:params]['data']
25
21
  action = args[:params]['action']
26
22
 
27
- collection.execute(caller, action, data, filter).to_json
23
+ collection.execute(args[:caller], action, data, filter)
28
24
  end
29
25
  end
30
26
  end
@@ -12,24 +12,17 @@ module ForestAdminRpcAgent
12
12
  end
13
13
 
14
14
  def handle_request(args)
15
- return '{}' unless args[:params]['collection_name']
15
+ return {} unless args[:params]['collection_name']
16
16
 
17
17
  datasource = ForestAdminRpcAgent::Facades::Container.datasource
18
18
  collection = datasource.get_collection(args[:params]['collection_name'])
19
-
20
- caller = if args[:params].key?('caller')
21
- ForestAdminDatasourceToolkit::Components::Caller.new(
22
- **args[:params]['caller'].to_h.transform_keys(&:to_sym)
23
- )
24
- end
25
19
  filter = FilterFactory.from_plain_object(args[:params]['filter'])
26
20
  metas = args[:params]['metas'] || {}
27
21
  data = args[:params]['data']
28
22
  action = args[:params]['action']
29
23
 
30
- form = collection.get_form(caller, action, data, filter, metas)
31
- form = encode_file_element(form)
32
- form.to_json
24
+ form = collection.get_form(args[:caller], action, data, filter, metas)
25
+ encode_file_element(form)
33
26
  end
34
27
 
35
28
  def encode_file_element(elements)
@@ -12,11 +12,8 @@ module ForestAdminRpcAgent
12
12
  end
13
13
 
14
14
  def handle_request(args)
15
- return '{}' unless args[:params]['collection_name']
15
+ return {} unless args[:params]['collection_name']
16
16
 
17
- caller = ForestAdminDatasourceToolkit::Components::Caller.new(
18
- **args[:params]['caller'].to_h.transform_keys(&:to_sym)
19
- )
20
17
  datasource = ForestAdminRpcAgent::Facades::Container.datasource
21
18
  collection = datasource.get_collection(args[:params]['collection_name'])
22
19
 
@@ -27,7 +24,7 @@ module ForestAdminRpcAgent
27
24
  )
28
25
  filter = FilterFactory.from_plain_object(args[:params]['filter'])
29
26
 
30
- collection.aggregate(caller, filter, aggregation, args[:params]['limit']).to_json
27
+ collection.aggregate(args[:caller], filter, aggregation, args[:params]['limit'])
31
28
  end
32
29
  end
33
30
  end
@@ -20,7 +20,8 @@ module ForestAdminRpcAgent
20
20
 
21
21
  def register_sinatra(app)
22
22
  app.send(@method.to_sym, @url) do
23
- handle_request(params)
23
+ result = handle_request(params)
24
+ serialize_response(result)
24
25
  end
25
26
  end
26
27
 
@@ -33,8 +34,8 @@ module ForestAdminRpcAgent
33
34
 
34
35
  if status == 200
35
36
  params = request.query_parameters.merge(request.request_parameters)
36
-
37
- [200, { 'Content-Type' => 'application/json' }, [handle_request({ params: params })]]
37
+ result = handle_request({ params: params, caller: headers[:caller] })
38
+ [200, { 'Content-Type' => 'application/json' }, [serialize_response(result)]]
38
39
  else
39
40
  [status, headers, response]
40
41
  end
@@ -47,6 +48,14 @@ module ForestAdminRpcAgent
47
48
  as: @name,
48
49
  route_alias: @name
49
50
  end
51
+
52
+ private
53
+
54
+ def serialize_response(result)
55
+ return result if result.is_a?(String) && (result.start_with?('{', '['))
56
+
57
+ result.to_json
58
+ end
50
59
  end
51
60
  end
52
61
  end
@@ -10,17 +10,13 @@ module ForestAdminRpcAgent
10
10
  end
11
11
 
12
12
  def handle_request(args)
13
- return '{}' unless args[:params]['collection_name']
13
+ return {} unless args[:params]['collection_name']
14
14
 
15
- chart_name = args[:params]['name']
16
- caller = ForestAdminDatasourceToolkit::Components::Caller.new(
17
- **args[:params]['caller'].to_h.transform_keys(&:to_sym)
18
- )
15
+ chart_name = args[:params]['chart']
19
16
  datasource = ForestAdminRpcAgent::Facades::Container.datasource
20
17
  collection = datasource.get_collection(args[:params]['collection_name'])
21
18
 
22
- primary_key_values = ForestAdminAgent::Utils::Id.unpack_id(collection, args[:params]['record_id'])
23
- collection.render_chart(caller, chart_name, primary_key_values)
19
+ collection.render_chart(caller, chart_name, args[:params]['record_id'])
24
20
  end
25
21
  end
26
22
  end
@@ -8,15 +8,12 @@ module ForestAdminRpcAgent
8
8
  end
9
9
 
10
10
  def handle_request(args)
11
- return '{}' unless args[:params]['collection_name']
11
+ return {} unless args[:params]['collection_name']
12
12
 
13
- caller = ForestAdminDatasourceToolkit::Components::Caller.new(
14
- **args[:params]['caller'].to_h.transform_keys(&:to_sym)
15
- )
16
13
  datasource = ForestAdminRpcAgent::Facades::Container.datasource
17
14
  collection = datasource.get_collection(args[:params]['collection_name'])
18
15
 
19
- [collection.create(caller, args[:params]['data'])].to_json
16
+ [collection.create(args[:caller], args[:params]['data'].first)]
20
17
  end
21
18
  end
22
19
  end
@@ -8,15 +8,12 @@ module ForestAdminRpcAgent
8
8
  end
9
9
 
10
10
  def handle_request(args)
11
- return '{}' unless args[:params]['chart']
11
+ return {} unless args[:params]['chart']
12
12
 
13
13
  chart_name = args[:params]['chart']
14
- caller = ForestAdminDatasourceToolkit::Components::Caller.new(
15
- **args[:params]['caller'].to_h.transform_keys(&:to_sym)
16
- )
17
14
  datasource = ForestAdminRpcAgent::Facades::Container.datasource
18
15
 
19
- datasource.render_chart(caller, chart_name).to_json
16
+ datasource.render_chart(args[:caller], chart_name)
20
17
  end
21
18
  end
22
19
  end
@@ -12,16 +12,13 @@ module ForestAdminRpcAgent
12
12
  end
13
13
 
14
14
  def handle_request(args)
15
- return '{}' unless args[:params]['collection_name']
15
+ return {} unless args[:params]['collection_name']
16
16
 
17
- caller = ForestAdminDatasourceToolkit::Components::Caller.new(
18
- **args[:params]['caller'].to_h.transform_keys(&:to_sym)
19
- )
20
17
  datasource = ForestAdminRpcAgent::Facades::Container.datasource
21
18
  collection = datasource.get_collection(args[:params]['collection_name'])
22
19
  filter = FilterFactory.from_plain_object(args[:params]['filter'])
23
20
 
24
- collection.delete(caller, filter).to_json
21
+ collection.delete(args[:caller], filter)
25
22
  end
26
23
  end
27
24
  end
@@ -12,17 +12,14 @@ module ForestAdminRpcAgent
12
12
  end
13
13
 
14
14
  def handle_request(args)
15
- return '{}' unless args[:params]['collection_name']
15
+ return {} unless args[:params]['collection_name']
16
16
 
17
- caller = ForestAdminDatasourceToolkit::Components::Caller.new(
18
- **args[:params]['caller'].to_h.transform_keys(&:to_sym)
19
- )
20
17
  datasource = ForestAdminRpcAgent::Facades::Container.datasource
21
18
  collection = datasource.get_collection(args[:params]['collection_name'])
22
19
  projection = Projection.new(args[:params]['projection'])
23
20
  filter = FilterFactory.from_plain_object(args[:params]['filter'])
24
21
 
25
- collection.list(caller, filter, projection).to_json
22
+ collection.list(args[:caller], filter, projection)
26
23
  end
27
24
  end
28
25
  end
@@ -8,14 +8,14 @@ module ForestAdminRpcAgent
8
8
  end
9
9
 
10
10
  def handle_request(args)
11
- return '{}' unless args[:params]['connection_name'] && args[:params]['query']
11
+ return {} unless args[:params]['connection_name'] && args[:params]['query']
12
12
 
13
13
  connection_name = args[:params]['connection_name']
14
14
  query = args[:params]['query']
15
15
  binds = args[:params]['binds'] || []
16
16
  datasource = ForestAdminRpcAgent::Facades::Container.datasource
17
17
 
18
- datasource.execute_native_query(connection_name, query, binds).to_json
18
+ datasource.execute_native_query(connection_name, query, binds)
19
19
  end
20
20
  end
21
21
  end
@@ -18,15 +18,10 @@ module ForestAdminRpcAgent
18
18
  .map { |_name, collection| collection.schema.merge({ name: collection.name }) }
19
19
  .sort_by { |collection| collection[:name] }
20
20
 
21
- connections = []
22
- agent.customizer.datasources.each do |root_datasource|
23
- connections = connections.union(
24
- root_datasource.live_query_connections.keys.map { |connection_name| { name: connection_name } }
25
- )
26
- end
21
+ connections = datasource.live_query_connections.keys.map { |connection_name| { name: connection_name } }
27
22
  schema[:native_query_connections] = connections
28
23
 
29
- schema.to_json
24
+ schema
30
25
  end
31
26
  end
32
27
  end
@@ -5,7 +5,7 @@ module ForestAdminRpcAgent
5
5
  class Sse
6
6
  DEFAULT_HEARTBEAT_INTERVAL = 1
7
7
 
8
- def initialize(url = 'rpc-sse', method = 'get', name = 'rpc_sse', heartbeat_interval: DEFAULT_HEARTBEAT_INTERVAL)
8
+ def initialize(url = 'sse', method = 'get', name = 'rpc_sse', heartbeat_interval: DEFAULT_HEARTBEAT_INTERVAL)
9
9
  @url = url
10
10
  @method = method
11
11
  @name = name
@@ -10,16 +10,13 @@ module ForestAdminRpcAgent
10
10
  end
11
11
 
12
12
  def handle_request(args)
13
- return '{}' unless args[:params]['collection_name']
13
+ return {} unless args[:params]['collection_name']
14
14
 
15
- caller = ForestAdminDatasourceToolkit::Components::Caller.new(
16
- **args[:params]['caller'].to_h.transform_keys(&:to_sym)
17
- )
18
15
  datasource = ForestAdminRpcAgent::Facades::Container.datasource
19
16
  collection = datasource.get_collection(args[:params]['collection_name'])
20
17
  filter = FilterFactory.from_plain_object(args[:params]['filter'])
21
18
 
22
- collection.update(caller, filter, args[:params]['data']).to_json
19
+ collection.update(args[:caller], filter, args[:params]['patch'])
23
20
  end
24
21
  end
25
22
  end
@@ -1,3 +1,3 @@
1
1
  module ForestAdminRpcAgent
2
- VERSION = "1.13.3"
2
+ VERSION = "1.14.0"
3
3
  end
@@ -23,6 +23,7 @@ module ForestAdminRpcAgent
23
23
  setting :logger_level, default: 'info'
24
24
  setting :logger, default: nil
25
25
  setting :customize_error_message, default: nil
26
+ setting :disable_route_cache, default: false
26
27
 
27
28
  begin
28
29
  require 'thor'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: forest_admin_rpc_agent
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.13.3
4
+ version: 1.14.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Matthieu
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2025-11-05 00:00:00.000000000 Z
12
+ date: 2025-11-07 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: base64
@@ -162,6 +162,7 @@ files:
162
162
  - lib/forest_admin_rpc_agent/extensions/config_loader.rb
163
163
  - lib/forest_admin_rpc_agent/extensions/sinatra_extension.rb
164
164
  - lib/forest_admin_rpc_agent/facades/container.rb
165
+ - lib/forest_admin_rpc_agent/http/router.rb
165
166
  - lib/forest_admin_rpc_agent/middleware/authentication.rb
166
167
  - lib/forest_admin_rpc_agent/routes/action_execute.rb
167
168
  - lib/forest_admin_rpc_agent/routes/action_form.rb