forest_admin_rpc_agent 1.16.10 → 1.17.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:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9aacf1c4f41b79f86b3b7f3a00caa96f546cc67d38031f7f550e3cb5ae4ec232
|
|
4
|
+
data.tar.gz: bf9a970a7322632045f32487767cd0e50c8b1087c2dc702ddd01cad900f5b7bc
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c8c65145002a3c4adea8332210a86e257de9a16bcc7bfecfc8ef7bab6d94cc1dafc9adddc7ea378854d1d55a5db1f2716a0110253e02e87aadfdc42b52231f3c
|
|
7
|
+
data.tar.gz: b148bfcf994516aee7a7d72bfdaac13cdab37a222fb6181798a162589e390819563eedfd8eca4ca1e01832882c1043058d805e0478a1e0872f27fbcfbe767f97
|
|
@@ -1,19 +1,260 @@
|
|
|
1
|
+
require 'digest'
|
|
2
|
+
require 'fileutils'
|
|
3
|
+
|
|
1
4
|
module ForestAdminRpcAgent
|
|
2
|
-
class Agent < ForestAdminAgent::Builder::AgentFactory
|
|
3
|
-
|
|
5
|
+
class Agent < ForestAdminAgent::Builder::AgentFactory # rubocop:disable Metrics/ClassLength
|
|
6
|
+
include ForestAdminAgent::Http::Exceptions
|
|
7
|
+
|
|
8
|
+
attr_reader :rpc_collections, :cached_schema, :cached_schema_hash
|
|
4
9
|
|
|
5
10
|
def setup(options)
|
|
6
11
|
super
|
|
7
12
|
@rpc_collections = []
|
|
13
|
+
@cached_schema = nil
|
|
14
|
+
@cached_schema_hash = nil
|
|
8
15
|
end
|
|
9
16
|
|
|
10
|
-
def send_schema(
|
|
11
|
-
|
|
17
|
+
def send_schema(force: false)
|
|
18
|
+
if should_skip_schema_update? && !force
|
|
19
|
+
log_schema_skip
|
|
20
|
+
load_and_cache_schema
|
|
21
|
+
return
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
schema_path = ForestAdminRpcAgent::Facades::Container.cache(:schema_path)
|
|
25
|
+
|
|
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
|
|
33
|
+
|
|
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
|
|
48
|
+
|
|
49
|
+
ForestAdminRpcAgent::Facades::Container.logger.log(
|
|
50
|
+
'Info',
|
|
51
|
+
'RPC agent does not send schema to Forest Admin servers.'
|
|
52
|
+
)
|
|
12
53
|
end
|
|
13
54
|
|
|
14
55
|
def mark_collections_as_rpc(*names)
|
|
15
56
|
@rpc_collections.push(*names)
|
|
16
57
|
self
|
|
17
58
|
end
|
|
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
|
+
|
|
69
|
+
# Check if provided hash matches the cached schema hash
|
|
70
|
+
def schema_hash_matches?(provided_hash)
|
|
71
|
+
return false unless @cached_schema_hash && provided_hash
|
|
72
|
+
|
|
73
|
+
@cached_schema_hash == provided_hash
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
private
|
|
77
|
+
|
|
78
|
+
def should_skip_schema_update?
|
|
79
|
+
ForestAdminRpcAgent::Facades::Container.cache(:skip_schema_update) == true
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def log_schema_skip
|
|
83
|
+
logger = ForestAdminRpcAgent::Facades::Container.logger
|
|
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")
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def load_and_cache_schema
|
|
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
|
+
|
|
120
|
+
FileUtils.mkdir_p(File.dirname(schema_path))
|
|
121
|
+
File.write(schema_path, format_schema_json(schema))
|
|
122
|
+
|
|
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
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def build_and_cache_schema_from_datasource
|
|
129
|
+
datasource = @container.resolve(:datasource)
|
|
130
|
+
|
|
131
|
+
@cached_schema = build_rpc_schema_from_datasource(datasource)
|
|
132
|
+
compute_and_cache_hash
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def build_rpc_schema_from_datasource(datasource)
|
|
136
|
+
schema = customizer.schema
|
|
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) }
|
|
140
|
+
schema[:collections] = collections.sort_by { |c| c[:name] }
|
|
141
|
+
|
|
142
|
+
connections = datasource.live_query_connections.keys.map { |connection_name| { name: connection_name } }
|
|
143
|
+
schema[:native_query_connections] = connections
|
|
144
|
+
|
|
145
|
+
schema
|
|
146
|
+
end
|
|
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
|
+
|
|
249
|
+
def compute_and_cache_hash
|
|
250
|
+
return unless @cached_schema
|
|
251
|
+
|
|
252
|
+
@cached_schema_hash = Digest::SHA1.hexdigest(@cached_schema.to_json)
|
|
253
|
+
|
|
254
|
+
ForestAdminRpcAgent::Facades::Container.logger.log(
|
|
255
|
+
'Debug',
|
|
256
|
+
"RPC agent schema hash computed: #{@cached_schema_hash}"
|
|
257
|
+
)
|
|
258
|
+
end
|
|
18
259
|
end
|
|
19
260
|
end
|
|
@@ -20,10 +20,12 @@ module ForestAdminRpcAgent
|
|
|
20
20
|
|
|
21
21
|
def register_sinatra(app)
|
|
22
22
|
app.send(@method.to_sym, @url) do
|
|
23
|
-
result = handle_request(params)
|
|
23
|
+
result = handle_request({ params: params, request: request })
|
|
24
24
|
|
|
25
25
|
if result.is_a?(Hash) && result.key?(:status)
|
|
26
26
|
status result[:status]
|
|
27
|
+
# Set custom headers if provided
|
|
28
|
+
result[:headers]&.each { |key, value| headers[key] = value }
|
|
27
29
|
result[:content] ? serialize_response(result[:content]) : ''
|
|
28
30
|
else
|
|
29
31
|
serialize_response(result)
|
|
@@ -38,16 +40,16 @@ module ForestAdminRpcAgent
|
|
|
38
40
|
# Skip authentication for health check (root path)
|
|
39
41
|
if @url == '/'
|
|
40
42
|
params = request.query_parameters.merge(request.request_parameters)
|
|
41
|
-
result = handle_request({ params: params, caller: nil })
|
|
42
|
-
|
|
43
|
+
result = handle_request({ params: params, caller: nil, request: request })
|
|
44
|
+
build_rails_response(result)
|
|
43
45
|
else
|
|
44
46
|
auth_middleware = ForestAdminRpcAgent::Middleware::Authentication.new(->(_env) { [200, {}, ['OK']] })
|
|
45
47
|
status, headers, response = auth_middleware.call(request.env)
|
|
46
48
|
|
|
47
49
|
if status == 200
|
|
48
50
|
params = request.query_parameters.merge(request.request_parameters)
|
|
49
|
-
result = handle_request({ params: params, caller: headers[:caller] })
|
|
50
|
-
|
|
51
|
+
result = handle_request({ params: params, caller: headers[:caller], request: request })
|
|
52
|
+
build_rails_response(result)
|
|
51
53
|
else
|
|
52
54
|
[status, headers, response]
|
|
53
55
|
end
|
|
@@ -62,6 +64,17 @@ module ForestAdminRpcAgent
|
|
|
62
64
|
route_alias: @name
|
|
63
65
|
end
|
|
64
66
|
|
|
67
|
+
def build_rails_response(result)
|
|
68
|
+
if result.is_a?(Hash) && result.key?(:status)
|
|
69
|
+
response_headers = { 'Content-Type' => 'application/json' }
|
|
70
|
+
response_headers.merge!(result[:headers]) if result[:headers]
|
|
71
|
+
body = result[:content] ? serialize_response(result[:content]) : ''
|
|
72
|
+
[result[:status], response_headers, [body]]
|
|
73
|
+
else
|
|
74
|
+
[200, { 'Content-Type' => 'application/json' }, [serialize_response(result)]]
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
65
78
|
protected
|
|
66
79
|
|
|
67
80
|
def get_collection_safe(datasource, collection_name)
|
|
@@ -5,23 +5,66 @@ module ForestAdminRpcAgent
|
|
|
5
5
|
include ForestAdminAgent::Utils
|
|
6
6
|
include ForestAdminAgent::Routes::QueryHandler
|
|
7
7
|
|
|
8
|
+
HTTP_OK = 200
|
|
9
|
+
HTTP_NOT_MODIFIED = 304
|
|
10
|
+
|
|
8
11
|
def initialize
|
|
9
12
|
super('rpc-schema', 'get', 'rpc_schema')
|
|
10
13
|
end
|
|
11
14
|
|
|
12
|
-
def handle_request(
|
|
15
|
+
def handle_request(args)
|
|
13
16
|
agent = ForestAdminRpcAgent::Agent.instance
|
|
14
|
-
|
|
15
|
-
|
|
17
|
+
client_etag = extract_if_none_match(args)
|
|
18
|
+
|
|
19
|
+
# If client has cached schema and ETag matches, return 304 Not Modified
|
|
20
|
+
if client_etag && agent.schema_hash_matches?(client_etag)
|
|
21
|
+
ForestAdminRpcAgent::Facades::Container.logger.log(
|
|
22
|
+
'Debug',
|
|
23
|
+
'ETag matches, returning 304 Not Modified'
|
|
24
|
+
)
|
|
25
|
+
return { status: HTTP_NOT_MODIFIED, content: nil,
|
|
26
|
+
headers: { 'ETag' => quote_etag(agent.cached_schema_hash) } }
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Get schema from cache (or build from datasource if not cached)
|
|
30
|
+
schema = agent.rpc_schema
|
|
31
|
+
etag = agent.cached_schema_hash
|
|
32
|
+
|
|
33
|
+
# Return schema with ETag header
|
|
34
|
+
{
|
|
35
|
+
status: HTTP_OK,
|
|
36
|
+
content: schema,
|
|
37
|
+
headers: { 'ETag' => quote_etag(etag) }
|
|
38
|
+
}
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
16
42
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
43
|
+
def extract_if_none_match(args)
|
|
44
|
+
request = args[:request] if args.is_a?(Hash)
|
|
45
|
+
return nil unless request
|
|
46
|
+
|
|
47
|
+
# Get If-None-Match header (works for both Rails and Sinatra)
|
|
48
|
+
etag = if request.respond_to?(:get_header)
|
|
49
|
+
request.get_header('HTTP_IF_NONE_MATCH')
|
|
50
|
+
elsif request.respond_to?(:env)
|
|
51
|
+
request.env['HTTP_IF_NONE_MATCH']
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Strip quotes from ETag value if present
|
|
55
|
+
unquote_etag(etag)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def quote_etag(etag)
|
|
59
|
+
return nil unless etag
|
|
60
|
+
|
|
61
|
+
%("#{etag}")
|
|
62
|
+
end
|
|
20
63
|
|
|
21
|
-
|
|
22
|
-
|
|
64
|
+
def unquote_etag(etag)
|
|
65
|
+
return nil unless etag
|
|
23
66
|
|
|
24
|
-
|
|
67
|
+
etag.gsub(/\A"?|"?\z/, '')
|
|
25
68
|
end
|
|
26
69
|
end
|
|
27
70
|
end
|
|
@@ -20,6 +20,8 @@ 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-schema.json')
|
|
24
|
+
setting :skip_schema_update, default: false
|
|
23
25
|
setting :logger_level, default: 'info'
|
|
24
26
|
setting :logger, default: nil
|
|
25
27
|
setting :customize_error_message, default: nil
|
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.
|
|
4
|
+
version: 1.17.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-12-
|
|
12
|
+
date: 2025-12-11 00:00:00.000000000 Z
|
|
13
13
|
dependencies:
|
|
14
14
|
- !ruby/object:Gem::Dependency
|
|
15
15
|
name: base64
|