forest_admin_rpc_agent 1.23.0 → 2.0.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: 281746bde0e28c1eeedba1e49b856eea8c708e08c1eb6c7f6e4ae3525b064798
4
- data.tar.gz: 7b0b539680aca45fd32e5e3b9ec2238fa46d074cb4f85eb13b10fdd36cc1440e
3
+ metadata.gz: '06883f603b147464ddf1ad642f47af2c3230b41cd043cf663ad9214ad22f98ec'
4
+ data.tar.gz: 13cdf53f4abc7147fd38d73efa4ee74f7829abbf320ce363ac315f42e8f1e322
5
5
  SHA512:
6
- metadata.gz: 1bc9659f90752406775d2cb4501be57c5ec24b134d9dcbad1386d0ac3c07435a3e332f37ef1213ba4060dd3af85b2754d8bd8e57b9cd00dcdd9748162753e762
7
- data.tar.gz: ba50623fff5d14f03c4a4d43ff10def285eb9a73cabd4d1424f8495677bf5d6566a291aac3c124e4559d71013a7b88a801a66c56645018727dd51ab7f5a5c322
6
+ metadata.gz: 2e680dc41c11c375aafe885afee8870b78ee67c81b9853f33c8eb214f1ddd4a60e82c4fe11bbd78f30c894c7288ad66f211c1d354cc7c780fb4c22e5abe4cb48
7
+ data.tar.gz: 02e09ab9ba06a77619dcd42785c3965f578e799014517a85535234c6079e673d4d31b0491fced672764b82fa7186022394b8570415d63f7797a4cc7ee2d9e760
@@ -31,12 +31,9 @@ admin work on any Ruby application."
31
31
  spec.require_paths = ["lib"]
32
32
 
33
33
  spec.add_dependency "base64"
34
- spec.add_dependency "benchmark"
35
34
  spec.add_dependency "bigdecimal"
36
- spec.add_dependency "cgi"
37
35
  spec.add_dependency "csv"
38
36
  spec.add_dependency "dry-configurable", "~> 1.1"
39
- spec.add_dependency "logger"
40
37
  spec.add_dependency "mutex_m"
41
38
  spec.add_dependency "ostruct"
42
39
  spec.add_dependency "thor", "~> 1.3"
@@ -2,7 +2,7 @@ require 'digest'
2
2
  require 'fileutils'
3
3
 
4
4
  module ForestAdminRpcAgent
5
- class Agent < ForestAdminAgent::Builder::AgentFactory
5
+ class Agent < ForestAdminAgent::Builder::AgentFactory # rubocop:disable Metrics/ClassLength
6
6
  include ForestAdminAgent::Http::Exceptions
7
7
 
8
8
  attr_reader :rpc_collections, :cached_schema, :cached_schema_hash
@@ -12,37 +12,40 @@ module ForestAdminRpcAgent
12
12
  @rpc_collections = []
13
13
  @cached_schema = nil
14
14
  @cached_schema_hash = nil
15
- @customizer = ForestAdminRpcAgent::DatasourceCustomizer.new
16
- end
17
-
18
- def add_datasource(datasource, options = {})
19
- if options[:mark_collections_as_rpc]
20
- options[:mark_collections_callback] = ->(ds) { mark_collections_as_rpc(*ds.collections.keys) }
21
- end
22
-
23
- super
24
15
  end
25
16
 
26
17
  def send_schema(force: false)
27
18
  if should_skip_schema_update? && !force
28
19
  log_schema_skip
20
+ load_and_cache_schema
29
21
  return
30
22
  end
31
23
 
32
- datasource = @container.resolve(:datasource)
24
+ schema_path = ForestAdminRpcAgent::Facades::Container.cache(:schema_path)
33
25
 
34
- # Build and cache RPC schema from live datasource
35
- @cached_schema = build_rpc_schema_from_datasource(datasource)
36
- compute_and_cache_hash
26
+ if ForestAdminRpcAgent::Facades::Container.cache(:is_production)
27
+ unless schema_path && File.exist?(schema_path)
28
+ raise InternalServerError.new(
29
+ 'Schema file not found in production',
30
+ details: { schema_path: schema_path }
31
+ )
32
+ end
37
33
 
38
- # Write schema file for reference (only in development mode)
39
- # Uses the same serialization as the /rpc-schema route
40
- write_schema_file_for_reference unless ForestAdminRpcAgent::Facades::Container.cache(:is_production)
34
+ load_and_cache_schema_from_file(schema_path)
35
+
36
+ ForestAdminRpcAgent::Facades::Container.logger.log(
37
+ 'Info',
38
+ 'RPC agent running in production mode, using existing schema file.'
39
+ )
40
+ else
41
+ generate_and_cache_schema(schema_path)
42
+
43
+ ForestAdminRpcAgent::Facades::Container.logger.log(
44
+ 'Info',
45
+ "RPC agent schema generated and saved to #{schema_path}"
46
+ )
47
+ end
41
48
 
42
- ForestAdminRpcAgent::Facades::Container.logger.log(
43
- 'Info',
44
- 'RPC agent schema computed from datasource and cached.'
45
- )
46
49
  ForestAdminRpcAgent::Facades::Container.logger.log(
47
50
  'Info',
48
51
  'RPC agent does not send schema to Forest Admin servers.'
@@ -54,6 +57,15 @@ module ForestAdminRpcAgent
54
57
  self
55
58
  end
56
59
 
60
+ # Returns the cached schema for the /rpc-schema route
61
+ # Falls back to building schema from datasource if not cached
62
+ def rpc_schema
63
+ return @cached_schema if @cached_schema
64
+
65
+ build_and_cache_schema_from_datasource
66
+ @cached_schema
67
+ end
68
+
57
69
  # Check if provided hash matches the cached schema hash
58
70
  def schema_hash_matches?(provided_hash)
59
71
  return false unless @cached_schema_hash && provided_hash
@@ -70,70 +82,170 @@ module ForestAdminRpcAgent
70
82
  def log_schema_skip
71
83
  logger = ForestAdminRpcAgent::Facades::Container.logger
72
84
  logger.log('Warn', '[ForestAdmin] Schema update skipped (skip_schema_update flag is true)')
85
+ environment = ForestAdminRpcAgent::Facades::Container.cache(:is_production) ? 'production' : 'development'
86
+ logger.log('Info', "[ForestAdmin] RPC agent running in #{environment} mode")
73
87
  end
74
88
 
75
- def write_schema_file_for_reference
89
+ def load_and_cache_schema
76
90
  schema_path = ForestAdminRpcAgent::Facades::Container.cache(:schema_path)
91
+
92
+ if ForestAdminRpcAgent::Facades::Container.cache(:is_production) && schema_path && File.exist?(schema_path)
93
+ load_and_cache_schema_from_file(schema_path)
94
+ else
95
+ # In development with skip_schema_update, still build from datasource
96
+ build_and_cache_schema_from_datasource
97
+ end
98
+ end
99
+
100
+ def load_and_cache_schema_from_file(_schema_path)
101
+ # File exists but RPC schema needs internal format - build from datasource
102
+ # The file is kept for reference/frontend but RPC always uses internal format
103
+ datasource = @container.resolve(:datasource)
104
+ @cached_schema = build_rpc_schema_from_datasource(datasource)
105
+ compute_and_cache_hash
106
+ end
107
+
108
+ def generate_and_cache_schema(schema_path)
109
+ datasource = @container.resolve(:datasource)
110
+
111
+ # Generate frontend schema for file (used by Forest Admin)
112
+ generated = ForestAdminAgent::Utils::Schema::SchemaEmitter.generate(datasource)
113
+ meta = ForestAdminAgent::Utils::Schema::SchemaEmitter.meta
114
+
115
+ schema = {
116
+ meta: meta,
117
+ collections: generated
118
+ }
119
+
77
120
  FileUtils.mkdir_p(File.dirname(schema_path))
78
- # Use the same serialization as the /rpc-schema route (.to_json)
79
- File.write(schema_path, JSON.pretty_generate(JSON.parse(@cached_schema.to_json)))
121
+ File.write(schema_path, format_schema_json(schema))
80
122
 
81
- ForestAdminRpcAgent::Facades::Container.logger.log(
82
- 'Info',
83
- "RPC agent schema file saved to #{schema_path}"
84
- )
123
+ # Build RPC schema in internal format (used by master agent)
124
+ @cached_schema = build_rpc_schema_from_datasource(datasource)
125
+ compute_and_cache_hash
85
126
  end
86
127
 
87
- def build_rpc_schema_from_datasource(datasource)
88
- schema = customizer.schema
128
+ def build_and_cache_schema_from_datasource
129
+ datasource = @container.resolve(:datasource)
89
130
 
90
- rpc_relations = {}
91
- collections = []
92
-
93
- datasource.collections.each_value do |collection|
94
- relations = {}
95
-
96
- if @rpc_collections.include?(collection.name)
97
- # RPC collection → extract relations to non-RPC collections
98
- collection.schema[:fields].each do |field_name, field|
99
- next if field.type == 'Column'
100
- next if @rpc_collections.include?(field.foreign_collection)
101
-
102
- relations[field_name] = field
103
- end
104
- else
105
- fields = {}
106
-
107
- collection.schema[:fields].each do |field_name, field|
108
- if field.type != 'Column' && @rpc_collections.include?(field.foreign_collection)
109
- relations[field_name] = field
110
- else
111
- if field.type == 'Column'
112
- field.filter_operators = ForestAdminAgent::Utils::Schema::FrontendFilterable.sort_operators(
113
- field.filter_operators
114
- )
115
- end
116
-
117
- fields[field_name] = field
118
- end
119
- end
120
-
121
- # Normal collection → include in schema
122
- collections << collection.schema.merge({ name: collection.name, fields: fields })
123
- end
131
+ @cached_schema = build_rpc_schema_from_datasource(datasource)
132
+ compute_and_cache_hash
133
+ end
124
134
 
125
- rpc_relations[collection.name] = relations unless relations.empty?
126
- end
135
+ def build_rpc_schema_from_datasource(datasource)
136
+ schema = customizer.schema
127
137
 
138
+ # Serialize collections with internal schema format (fields as hash with :type keys)
139
+ collections = datasource.collections.map { |_name, collection| serialize_collection_for_rpc(collection) }
128
140
  schema[:collections] = collections.sort_by { |c| c[:name] }
129
- schema[:rpc_relations] = rpc_relations
130
141
 
131
- schema[:native_query_connections] = datasource.live_query_connections.keys
132
- .map { |connection_name| { name: connection_name } }
142
+ connections = datasource.live_query_connections.keys.map { |connection_name| { name: connection_name } }
143
+ schema[:native_query_connections] = connections
133
144
 
134
145
  schema
135
146
  end
136
147
 
148
+ def serialize_collection_for_rpc(collection)
149
+ {
150
+ name: collection.name,
151
+ countable: collection.schema[:countable],
152
+ searchable: collection.schema[:searchable],
153
+ segments: collection.schema[:segments] || [],
154
+ charts: collection.schema[:charts] || [],
155
+ actions: serialize_actions_for_rpc(collection.schema[:actions] || {}),
156
+ fields: serialize_fields_for_rpc(collection.schema[:fields] || {})
157
+ }
158
+ end
159
+
160
+ def serialize_fields_for_rpc(fields)
161
+ fields.transform_values do |field_schema|
162
+ serialize_field_schema(field_schema)
163
+ end
164
+ end
165
+
166
+ def serialize_field_schema(field_schema)
167
+ case field_schema
168
+ when ForestAdminDatasourceToolkit::Schema::ColumnSchema
169
+ {
170
+ type: 'Column',
171
+ column_type: field_schema.column_type,
172
+ filter_operators: field_schema.filter_operators,
173
+ is_primary_key: field_schema.is_primary_key,
174
+ is_read_only: field_schema.is_read_only,
175
+ is_sortable: field_schema.is_sortable,
176
+ default_value: field_schema.default_value,
177
+ enum_values: field_schema.enum_values,
178
+ validation: field_schema.validation
179
+ }
180
+ when ForestAdminDatasourceToolkit::Schema::Relations::ManyToOneSchema
181
+ {
182
+ type: 'ManyToOne',
183
+ foreign_collection: field_schema.foreign_collection,
184
+ foreign_key: field_schema.foreign_key,
185
+ foreign_key_target: field_schema.foreign_key_target
186
+ }
187
+ when ForestAdminDatasourceToolkit::Schema::Relations::OneToOneSchema
188
+ {
189
+ type: 'OneToOne',
190
+ foreign_collection: field_schema.foreign_collection,
191
+ origin_key: field_schema.origin_key,
192
+ origin_key_target: field_schema.origin_key_target
193
+ }
194
+ when ForestAdminDatasourceToolkit::Schema::Relations::OneToManySchema
195
+ {
196
+ type: 'OneToMany',
197
+ foreign_collection: field_schema.foreign_collection,
198
+ origin_key: field_schema.origin_key,
199
+ origin_key_target: field_schema.origin_key_target
200
+ }
201
+ when ForestAdminDatasourceToolkit::Schema::Relations::ManyToManySchema
202
+ {
203
+ type: 'ManyToMany',
204
+ foreign_collection: field_schema.foreign_collection,
205
+ foreign_key: field_schema.foreign_key,
206
+ foreign_key_target: field_schema.foreign_key_target,
207
+ origin_key: field_schema.origin_key,
208
+ origin_key_target: field_schema.origin_key_target,
209
+ through_collection: field_schema.through_collection
210
+ }
211
+ when ForestAdminDatasourceToolkit::Schema::Relations::PolymorphicManyToOneSchema
212
+ {
213
+ type: 'PolymorphicManyToOne',
214
+ foreign_collections: field_schema.foreign_collections,
215
+ foreign_key: field_schema.foreign_key,
216
+ foreign_key_type_field: field_schema.foreign_key_type_field,
217
+ foreign_key_targets: field_schema.foreign_key_targets
218
+ }
219
+ when ForestAdminDatasourceToolkit::Schema::Relations::PolymorphicOneToOneSchema
220
+ {
221
+ type: 'PolymorphicOneToOne',
222
+ foreign_collection: field_schema.foreign_collection,
223
+ origin_key: field_schema.origin_key,
224
+ origin_key_target: field_schema.origin_key_target,
225
+ origin_type_field: field_schema.origin_type_field,
226
+ origin_type_value: field_schema.origin_type_value
227
+ }
228
+ when ForestAdminDatasourceToolkit::Schema::Relations::PolymorphicOneToManySchema
229
+ {
230
+ type: 'PolymorphicOneToMany',
231
+ foreign_collection: field_schema.foreign_collection,
232
+ origin_key: field_schema.origin_key,
233
+ origin_key_target: field_schema.origin_key_target,
234
+ origin_type_field: field_schema.origin_type_field,
235
+ origin_type_value: field_schema.origin_type_value
236
+ }
237
+ else
238
+ # Fallback: try to convert to hash if possible
239
+ field_schema.respond_to?(:to_h) ? field_schema.to_h : field_schema
240
+ end
241
+ end
242
+
243
+ def serialize_actions_for_rpc(actions)
244
+ actions.transform_values do |action|
245
+ action.respond_to?(:to_h) ? action.to_h : action
246
+ end
247
+ end
248
+
137
249
  def compute_and_cache_hash
138
250
  return unless @cached_schema
139
251
 
@@ -39,16 +39,16 @@ module ForestAdminRpcAgent
39
39
 
40
40
  # Skip authentication for health check (root path)
41
41
  if @url == '/'
42
- params = deep_symbolize_keys(request.query_parameters.merge(request.request_parameters))
43
- result = handle_request({ params: params.with_indifferent_access, caller: nil, request: request })
42
+ params = request.query_parameters.merge(request.request_parameters)
43
+ result = handle_request({ params: params, caller: nil, request: request })
44
44
  build_rails_response(result)
45
45
  else
46
46
  auth_middleware = ForestAdminRpcAgent::Middleware::Authentication.new(->(_env) { [200, {}, ['OK']] })
47
47
  status, headers, response = auth_middleware.call(request.env)
48
48
 
49
49
  if status == 200
50
- params = deep_symbolize_keys(request.query_parameters.merge(request.request_parameters))
51
- result = handle_request({ params: params.with_indifferent_access, caller: headers[:caller], request: request })
50
+ params = request.query_parameters.merge(request.request_parameters)
51
+ result = handle_request({ params: params, caller: headers[:caller], request: request })
52
52
  build_rails_response(result)
53
53
  else
54
54
  [status, headers, response]
@@ -87,17 +87,6 @@ module ForestAdminRpcAgent
87
87
 
88
88
  private
89
89
 
90
- def deep_symbolize_keys(obj)
91
- case obj
92
- when Hash
93
- obj.transform_keys(&:to_sym).transform_values { |v| deep_symbolize_keys(v) }
94
- when Array
95
- obj.map { |v| deep_symbolize_keys(v) }
96
- else
97
- obj
98
- end
99
- end
100
-
101
90
  def serialize_response(result)
102
91
  return result if result.is_a?(String) && (result.start_with?('{', '['))
103
92
 
@@ -27,7 +27,7 @@ module ForestAdminRpcAgent
27
27
  end
28
28
 
29
29
  # Get schema from cache (or build from datasource if not cached)
30
- schema = agent.cached_schema
30
+ schema = agent.rpc_schema
31
31
  etag = agent.cached_schema_hash
32
32
 
33
33
  # Return schema with ETag header
@@ -1,3 +1,3 @@
1
1
  module ForestAdminRpcAgent
2
- VERSION = "1.23.0"
2
+ VERSION = "2.0.0"
3
3
  end
@@ -20,13 +20,12 @@ module ForestAdminRpcAgent
20
20
  setting :prefix, default: nil
21
21
  setting :cache_dir, default: :'tmp/cache/forest_admin'
22
22
  setting :project_dir, default: Dir.pwd
23
- setting :schema_path, default: File.join(Dir.pwd, '.forestadmin-rpc-schema.json')
23
+ setting :schema_path, default: File.join(Dir.pwd, '.forestadmin-schema.json')
24
24
  setting :skip_schema_update, default: false
25
25
  setting :logger_level, default: 'info'
26
26
  setting :logger, default: nil
27
27
  setting :customize_error_message, default: nil
28
28
  setting :disable_route_cache, default: false
29
- setting :rpc_max_polling_threads, default: nil
30
29
 
31
30
  begin
32
31
  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.23.0
4
+ version: 2.0.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: 2026-01-26 00:00:00.000000000 Z
12
+ date: 2025-12-11 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: base64
@@ -25,20 +25,6 @@ dependencies:
25
25
  - - ">="
26
26
  - !ruby/object:Gem::Version
27
27
  version: '0'
28
- - !ruby/object:Gem::Dependency
29
- name: benchmark
30
- requirement: !ruby/object:Gem::Requirement
31
- requirements:
32
- - - ">="
33
- - !ruby/object:Gem::Version
34
- version: '0'
35
- type: :runtime
36
- prerelease: false
37
- version_requirements: !ruby/object:Gem::Requirement
38
- requirements:
39
- - - ">="
40
- - !ruby/object:Gem::Version
41
- version: '0'
42
28
  - !ruby/object:Gem::Dependency
43
29
  name: bigdecimal
44
30
  requirement: !ruby/object:Gem::Requirement
@@ -53,20 +39,6 @@ dependencies:
53
39
  - - ">="
54
40
  - !ruby/object:Gem::Version
55
41
  version: '0'
56
- - !ruby/object:Gem::Dependency
57
- name: cgi
58
- requirement: !ruby/object:Gem::Requirement
59
- requirements:
60
- - - ">="
61
- - !ruby/object:Gem::Version
62
- version: '0'
63
- type: :runtime
64
- prerelease: false
65
- version_requirements: !ruby/object:Gem::Requirement
66
- requirements:
67
- - - ">="
68
- - !ruby/object:Gem::Version
69
- version: '0'
70
42
  - !ruby/object:Gem::Dependency
71
43
  name: csv
72
44
  requirement: !ruby/object:Gem::Requirement
@@ -95,20 +67,6 @@ dependencies:
95
67
  - - "~>"
96
68
  - !ruby/object:Gem::Version
97
69
  version: '1.1'
98
- - !ruby/object:Gem::Dependency
99
- name: logger
100
- requirement: !ruby/object:Gem::Requirement
101
- requirements:
102
- - - ">="
103
- - !ruby/object:Gem::Version
104
- version: '0'
105
- type: :runtime
106
- prerelease: false
107
- version_requirements: !ruby/object:Gem::Requirement
108
- requirements:
109
- - - ">="
110
- - !ruby/object:Gem::Version
111
- version: '0'
112
70
  - !ruby/object:Gem::Dependency
113
71
  name: mutex_m
114
72
  requirement: !ruby/object:Gem::Requirement
@@ -200,7 +158,6 @@ files:
200
158
  - forest_admin_rpc_agent.gemspec
201
159
  - lib/forest_admin_rpc_agent.rb
202
160
  - lib/forest_admin_rpc_agent/agent.rb
203
- - lib/forest_admin_rpc_agent/datasource_customizer.rb
204
161
  - lib/forest_admin_rpc_agent/engine.rb
205
162
  - lib/forest_admin_rpc_agent/extensions/config_loader.rb
206
163
  - lib/forest_admin_rpc_agent/extensions/sinatra_extension.rb
@@ -1,27 +0,0 @@
1
- module ForestAdminRpcAgent
2
- class DatasourceCustomizer < ForestAdminDatasourceCustomizer::DatasourceCustomizer
3
- def add_datasource(datasource, options)
4
- @stack.queue_customization(lambda {
5
- if options[:include] || options[:exclude]
6
- publication_decorator = Decorators::Publication::PublicationDatasourceDecorator.new(datasource)
7
- publication_decorator.keep_collections_matching(options[:include], options[:exclude])
8
- datasource = publication_decorator
9
- end
10
-
11
- if options[:rename]
12
- rename_collection_decorator = Decorators::RenameCollection::RenameCollectionDatasourceDecorator.new(
13
- datasource
14
- )
15
- rename_collection_decorator.rename_collections(options[:rename])
16
- datasource = rename_collection_decorator
17
- end
18
-
19
- options[:mark_collections_callback]&.call(datasource)
20
-
21
- @composite_datasource.add_data_source(datasource)
22
- })
23
-
24
- self
25
- end
26
- end
27
- end