the-active-rpc 1.0.1
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 +7 -0
- data/CHANGELOG.md +82 -0
- data/LICENSE +21 -0
- data/README.md +59 -0
- data/Rakefile +8 -0
- data/lib/active_rpc/client_config.rb +30 -0
- data/lib/active_rpc/client_factory.rb +61 -0
- data/lib/active_rpc/configuration.rb +50 -0
- data/lib/active_rpc/model_extensions/attribute_dsl.rb +31 -0
- data/lib/active_rpc/model_extensions.rb +1456 -0
- data/lib/active_rpc/rpc/base_controller.rb +63 -0
- data/lib/active_rpc/rpc/concerns/includable.rb +68 -0
- data/lib/active_rpc/rpc/concerns/paginatable.rb +155 -0
- data/lib/active_rpc/rpc/concerns/query_builder.rb +178 -0
- data/lib/active_rpc/rpc/concerns/ransackable.rb +113 -0
- data/lib/active_rpc/rpc/concerns/request_processor.rb +191 -0
- data/lib/active_rpc/rpc/concerns/resource_controller.rb +281 -0
- data/lib/active_rpc/rpc/concerns/scopable.rb +92 -0
- data/lib/active_rpc/rpc/concerns/serializable.rb +30 -0
- data/lib/active_rpc/rpc/concerns/sortable.rb +71 -0
- data/lib/active_rpc/rpc/configuration.rb +7 -0
- data/lib/active_rpc/rpc/interceptors/locale_interceptor.rb +38 -0
- data/lib/active_rpc/rpc.rb +18 -0
- data/lib/active_rpc/version.rb +3 -0
- data/lib/active_rpc.rb +15 -0
- data/lib/generators/active_rpc/client_setup/client_setup_generator.rb +57 -0
- data/lib/generators/active_rpc/controller/gruf_controller_generator.rb +60 -0
- data/lib/generators/active_rpc/server_setup/server_setup_generator.rb +11 -0
- data/lib/the-active-rpc.rb +1 -0
- metadata +196 -0
|
@@ -0,0 +1,1456 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'ostruct'
|
|
4
|
+
require_relative 'model_extensions/attribute_dsl'
|
|
5
|
+
|
|
6
|
+
# This module provides a DSL for integrating gRPC services with ActiveRecord models
|
|
7
|
+
# It automatically creates methods to access attributes from gRPC responses
|
|
8
|
+
# IMPORTANT: This concern is designed to be used ONLY with ActiveRecord models
|
|
9
|
+
module ActiveRpc
|
|
10
|
+
module ModelExtensions
|
|
11
|
+
extend ActiveSupport::Concern
|
|
12
|
+
|
|
13
|
+
def self.prepare_value_for_rpc(value, type = nil)
|
|
14
|
+
prepared =
|
|
15
|
+
if type&.respond_to?(:rpc_serialize)
|
|
16
|
+
type.rpc_serialize(value)
|
|
17
|
+
elsif value.respond_to?(:rpc_serialize)
|
|
18
|
+
value.rpc_serialize
|
|
19
|
+
elsif value.respond_to?(:as_rpc_payload)
|
|
20
|
+
value.as_rpc_payload
|
|
21
|
+
elsif value.respond_to?(:as_json)
|
|
22
|
+
value.as_json
|
|
23
|
+
else
|
|
24
|
+
value
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
if defined?(ActiveSupport::HashWithIndifferentAccess) && prepared.is_a?(ActiveSupport::HashWithIndifferentAccess)
|
|
28
|
+
prepared = prepared.to_h
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
case prepared
|
|
32
|
+
when Hash
|
|
33
|
+
prepared.deep_stringify_keys
|
|
34
|
+
when Array
|
|
35
|
+
prepared.map { |item| prepare_value_for_rpc(item, nil) }
|
|
36
|
+
else
|
|
37
|
+
prepared
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
included do
|
|
42
|
+
# Include ActiveModel::Attributes for attribute management
|
|
43
|
+
include ActiveModel::Attributes
|
|
44
|
+
|
|
45
|
+
# Include ActiveModel::Dirty for change tracking
|
|
46
|
+
include ActiveModel::Dirty
|
|
47
|
+
|
|
48
|
+
# Class attribute to store the gRPC client for direct access
|
|
49
|
+
class_attribute :grpc_client, instance_writer: false
|
|
50
|
+
|
|
51
|
+
# Store remote attributes for proper initialization
|
|
52
|
+
class_attribute :remote_attributes, instance_writer: false, default: {}
|
|
53
|
+
|
|
54
|
+
# Override the reload method to also reload remote data
|
|
55
|
+
def reload(*args)
|
|
56
|
+
# Call the original reload method
|
|
57
|
+
result = super
|
|
58
|
+
|
|
59
|
+
# Reload remote data
|
|
60
|
+
load_remote_data if respond_to?(:load_remote_data)
|
|
61
|
+
|
|
62
|
+
# Return self for method chaining
|
|
63
|
+
result
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Define a method to load all remote data
|
|
67
|
+
def load_remote_data
|
|
68
|
+
# Return early if active_rpc is not configured for this model
|
|
69
|
+
return unless self.class.respond_to?(:active_rpc_config) && self.class.active_rpc_config
|
|
70
|
+
|
|
71
|
+
# Get all active_rpc configurations
|
|
72
|
+
self.class.active_rpc_config&.dig(:options, :method_name).then do |method_name|
|
|
73
|
+
# Load the remote data
|
|
74
|
+
send("load_#{method_name}") if method_name && respond_to?("load_#{method_name}")
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
class_methods do
|
|
80
|
+
# Helper method to make locale-aware gRPC calls
|
|
81
|
+
# This ensures proper I18N support for localized models
|
|
82
|
+
def grpc_call_with_locale(method, params = {})
|
|
83
|
+
current_locale = defined?(I18n) ? I18n.locale&.to_s : nil
|
|
84
|
+
Rails.logger.debug "[ACTIVE_RPC DEBUG] grpc_call_with_locale: method=#{method}, locale=#{current_locale}"
|
|
85
|
+
|
|
86
|
+
begin
|
|
87
|
+
if current_locale && !current_locale.empty?
|
|
88
|
+
# Create metadata hash with locale (ensure string values)
|
|
89
|
+
metadata = { 'locale' => current_locale.to_s }
|
|
90
|
+
result = grpc_client.call(method, params, metadata)
|
|
91
|
+
else
|
|
92
|
+
result = grpc_client.call(method, params)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
result
|
|
96
|
+
rescue => e
|
|
97
|
+
raise e
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# DSL method to configure gRPC integration
|
|
102
|
+
# Example: active_rpc :core, :User, foreign_key: :user_id do
|
|
103
|
+
# define_rpc_attribute :name, :string, default: "Anonymous"
|
|
104
|
+
# define_rpc_attribute :email, :string
|
|
105
|
+
# define_rpc_attribute :roles, :json, default: []
|
|
106
|
+
# end
|
|
107
|
+
def active_rpc(service, resource, options = {}, &block)
|
|
108
|
+
# Create a configuration object to store attributes
|
|
109
|
+
config = OpenStruct.new(
|
|
110
|
+
service: service,
|
|
111
|
+
resource: resource,
|
|
112
|
+
attributes: [],
|
|
113
|
+
attribute_types: {},
|
|
114
|
+
attribute_defaults: {},
|
|
115
|
+
query_config: {},
|
|
116
|
+
scopes: {},
|
|
117
|
+
where_rpc_mappings: {},
|
|
118
|
+
options: options
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
# Create a DSL context for defining attributes
|
|
122
|
+
if block_given?
|
|
123
|
+
dsl = AttributeDsl.new(config)
|
|
124
|
+
dsl.instance_eval(&block)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Get the final list of attributes
|
|
128
|
+
attributes = config.attributes
|
|
129
|
+
rpc_attribute_names = attributes.map(&:to_s)
|
|
130
|
+
# Default options
|
|
131
|
+
options = {
|
|
132
|
+
foreign_key: :"#{resource.to_s.underscore}_id",
|
|
133
|
+
method_name: :"#{resource.to_s.underscore}_data",
|
|
134
|
+
reload_method: :"reload_#{resource.to_s.underscore}",
|
|
135
|
+
cache_expires_in: 30.minutes,
|
|
136
|
+
bulk_method: :"list_#{resource.to_s.pluralize.underscore}"
|
|
137
|
+
}.merge(options)
|
|
138
|
+
|
|
139
|
+
# Store configuration in class instance variable for better inheritance support
|
|
140
|
+
@active_rpc_config = {
|
|
141
|
+
service: service,
|
|
142
|
+
resource: resource,
|
|
143
|
+
attributes: config.attributes,
|
|
144
|
+
attribute_types: config.attribute_types,
|
|
145
|
+
attribute_defaults: config.attribute_defaults,
|
|
146
|
+
query_config: config.query_config,
|
|
147
|
+
scopes: config.scopes,
|
|
148
|
+
where_rpc_mappings: config.where_rpc_mappings,
|
|
149
|
+
options: options
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
# Store in remote_attributes for proper initialization
|
|
153
|
+
self.remote_attributes = remote_attributes.merge(
|
|
154
|
+
resource.to_s.underscore.to_sym => {
|
|
155
|
+
attributes: config.attributes,
|
|
156
|
+
attribute_types: config.attribute_types,
|
|
157
|
+
attribute_defaults: config.attribute_defaults,
|
|
158
|
+
query_config: config.query_config,
|
|
159
|
+
scopes: config.scopes,
|
|
160
|
+
where_rpc_mappings: config.where_rpc_mappings,
|
|
161
|
+
service: service,
|
|
162
|
+
options: options
|
|
163
|
+
}
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
# Initialize class variables for validation errors
|
|
167
|
+
@last_validation_errors = nil
|
|
168
|
+
@last_validation_messages = nil
|
|
169
|
+
|
|
170
|
+
# Initialize the gRPC client for this model
|
|
171
|
+
begin
|
|
172
|
+
self.grpc_client = ActiveRpc::ClientFactory.instance.client_for(service.to_s, "#{resource}s")
|
|
173
|
+
rescue StandardError => e
|
|
174
|
+
raise e unless defined?(Rails)
|
|
175
|
+
|
|
176
|
+
Rails.logger.error("Failed to initialize gRPC client for #{service}:#{resource}: #{e.message}")
|
|
177
|
+
# In production, we'll use a dummy client to prevent app crash
|
|
178
|
+
if defined?(Rails.env) && Rails.env.production?
|
|
179
|
+
self.grpc_client = ActiveRpc::DummyClient.new(service.to_s, "#{resource}s")
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# Re-raise in non-Rails environments
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# Define method to access the configuration
|
|
186
|
+
define_singleton_method(:active_rpc_config) do
|
|
187
|
+
@active_rpc_config
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# Define attributes using ActiveModel::Attributes
|
|
191
|
+
config.attributes.each do |attr|
|
|
192
|
+
attr_name = attr.to_s
|
|
193
|
+
|
|
194
|
+
# Get the type for this attribute
|
|
195
|
+
attr_type = config.attribute_types[attr] || :string
|
|
196
|
+
|
|
197
|
+
# Get default value if specified
|
|
198
|
+
default_value = config.attribute_defaults[attr]
|
|
199
|
+
|
|
200
|
+
# Define the attribute with the appropriate type and default value
|
|
201
|
+
if !default_value.nil?
|
|
202
|
+
# Use the specified default value
|
|
203
|
+
attribute attr_name, attr_type, default: default_value
|
|
204
|
+
else
|
|
205
|
+
# No default value
|
|
206
|
+
attribute attr_name, attr_type
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# Define getter methods for all models
|
|
211
|
+
config.attributes.each do |attr|
|
|
212
|
+
attr_name = attr.to_s
|
|
213
|
+
|
|
214
|
+
# Override the getter method to check for cached values first
|
|
215
|
+
define_method(attr_name) do
|
|
216
|
+
# Check if we have a cached value from bulk loading
|
|
217
|
+
cached_var = "@#{attr_name}_cached"
|
|
218
|
+
cached_loaded_var = "@#{attr_name}_cached_loaded"
|
|
219
|
+
|
|
220
|
+
return instance_variable_get(cached_var) if instance_variable_get(cached_loaded_var)
|
|
221
|
+
|
|
222
|
+
super()
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# Add an after_find callback to load remote data when a model is loaded from the database
|
|
227
|
+
# This ensures that remote attributes are available when using Model.find
|
|
228
|
+
# BUT only if the data hasn't already been cached from bulk loading
|
|
229
|
+
after_find :"load_#{options[:method_name]}_if_needed"
|
|
230
|
+
|
|
231
|
+
# This callback is now handled by the combined before_save callback above
|
|
232
|
+
|
|
233
|
+
# Add a before_save callback to handle both creation and updates
|
|
234
|
+
before_save do
|
|
235
|
+
# Skip RPC operations when in local mode (e.g., during seeding)
|
|
236
|
+
if ActiveRpc.local_mode?
|
|
237
|
+
if defined?(Rails) && Rails.respond_to?(:logger)
|
|
238
|
+
Rails.logger.debug("[ActiveRpc Local Mode] Skipping RPC operations for #{resource} during save")
|
|
239
|
+
end
|
|
240
|
+
next
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
# Handle creation - if we have pending attributes but no foreign key
|
|
244
|
+
if send(options[:foreign_key]).blank? && send("has_#{resource.to_s.underscore}_updates?")
|
|
245
|
+
data = send("pending_#{resource.to_s.underscore}_updates")
|
|
246
|
+
|
|
247
|
+
# Only create if there are actual changes
|
|
248
|
+
result = send("create_and_associate_#{resource.to_s.underscore}", data)
|
|
249
|
+
|
|
250
|
+
# For debugging
|
|
251
|
+
if defined?(Rails) && Rails.respond_to?(:logger)
|
|
252
|
+
Rails.logger.debug("[ActiveRpc] create_and_associate result: #{result}, foreign_key: #{send(options[:foreign_key]).inspect}")
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
# If the creation failed, abort the save
|
|
256
|
+
unless result
|
|
257
|
+
if defined?(Rails) && Rails.respond_to?(:logger)
|
|
258
|
+
Rails.logger.error("[ActiveRpc] Failed to create and associate #{resource} before creating #{self.class.name}")
|
|
259
|
+
end
|
|
260
|
+
throw :abort
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
# Handle updates - if we have pending updates and a foreign key
|
|
265
|
+
if send("has_#{resource.to_s.underscore}_updates?") && send(options[:foreign_key]).present?
|
|
266
|
+
# Save the updates to the gRPC service
|
|
267
|
+
result = send("update_#{resource.to_s.underscore}")
|
|
268
|
+
|
|
269
|
+
# If the update failed, abort the save
|
|
270
|
+
throw :abort unless result
|
|
271
|
+
end
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
# Add a method to track changes to delegated attributes
|
|
275
|
+
define_method("#{resource.to_s.underscore}_changed?") do
|
|
276
|
+
# Check if any of the remote attributes have changed using ActiveModel::Dirty
|
|
277
|
+
attributes.any? { |attr| respond_to?("#{attr}_changed?") && send("#{attr}_changed?") }
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
# Define method to get data from the gRPC service
|
|
281
|
+
define_method(options[:method_name]) do
|
|
282
|
+
# Create a mock message with the current attribute values
|
|
283
|
+
message = OpenStruct.new(id: send(options[:foreign_key]))
|
|
284
|
+
|
|
285
|
+
# Add all attributes to the message
|
|
286
|
+
config.attributes.each do |attr|
|
|
287
|
+
# Get the current value of the attribute
|
|
288
|
+
message.send("#{attr}=", send(attr))
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
# Return a mock response with the message
|
|
292
|
+
OpenStruct.new(message: message)
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
# Define method to force reload data from the gRPC service
|
|
296
|
+
define_method(options[:reload_method]) do
|
|
297
|
+
send("load_#{options[:method_name]}")
|
|
298
|
+
self
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
# Define method to load data from the gRPC service
|
|
302
|
+
define_method("load_#{options[:method_name]}") do
|
|
303
|
+
foreign_key_value = send(options[:foreign_key])
|
|
304
|
+
return unless foreign_key_value.present?
|
|
305
|
+
|
|
306
|
+
# Fetch data from the gRPC service
|
|
307
|
+
data = send("fetch_#{options[:method_name]}_from_#{service}")
|
|
308
|
+
|
|
309
|
+
# Set attribute values directly
|
|
310
|
+
return unless data&.message
|
|
311
|
+
|
|
312
|
+
# Update attributes with the response data
|
|
313
|
+
if data.message.respond_to?(:to_h)
|
|
314
|
+
# Convert the response message to a hash
|
|
315
|
+
response_data = data.message.to_h
|
|
316
|
+
|
|
317
|
+
# Use intersection to get only the attributes that exist in both the response and our model
|
|
318
|
+
attrs_to_assign = response_data.slice(*attributes.map(&:to_sym))
|
|
319
|
+
|
|
320
|
+
# Assign the attributes in one go
|
|
321
|
+
assign_attributes(attrs_to_assign)
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
# Clear remote attribute changes since these are the initial values
|
|
325
|
+
clear_attribute_changes(rpc_attribute_names) if respond_to?(:clear_attribute_changes)
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
# Define conditional method to load data only if not already cached from bulk loading
|
|
329
|
+
define_method("load_#{options[:method_name]}_if_needed") do
|
|
330
|
+
# Check if data is already loaded (from bulk loading or previous individual load)
|
|
331
|
+
return if instance_variable_get("@#{options[:method_name]}_loaded")
|
|
332
|
+
|
|
333
|
+
# Check if we have bulk loading data available using the record's cache key
|
|
334
|
+
# This happens when the record was loaded as part of a with_user_data scope
|
|
335
|
+
cache_key = instance_variable_get(:@_bulk_loading_cache_key) || Thread.current[:active_rpc_current_cache_key]
|
|
336
|
+
if cache_key
|
|
337
|
+
# Set the cache key on this record for future use
|
|
338
|
+
instance_variable_set(:@_bulk_loading_cache_key, cache_key)
|
|
339
|
+
cache_hash = self.class.instance_variable_get(:@_bulk_loading_cache) || {}
|
|
340
|
+
cache_data = cache_hash[cache_key]
|
|
341
|
+
|
|
342
|
+
if cache_data && cache_data[:lookup]
|
|
343
|
+
foreign_key_value = send(options[:foreign_key])
|
|
344
|
+
remote_data = cache_data[:lookup][foreign_key_value.to_s] if foreign_key_value.present?
|
|
345
|
+
|
|
346
|
+
if remote_data
|
|
347
|
+
# Cache the gRPC response
|
|
348
|
+
mock_response = OpenStruct.new(message: remote_data)
|
|
349
|
+
instance_variable_set("@#{options[:method_name]}", mock_response)
|
|
350
|
+
instance_variable_set("@#{options[:method_name]}_loaded", true)
|
|
351
|
+
|
|
352
|
+
# Cache individual attributes
|
|
353
|
+
bulk_attributes = cache_data[:attributes] || attributes
|
|
354
|
+
bulk_attributes.each do |attr|
|
|
355
|
+
next unless remote_data.respond_to?(attr)
|
|
356
|
+
|
|
357
|
+
value = remote_data.send(attr)
|
|
358
|
+
instance_variable_set("@#{attr}_cached", value)
|
|
359
|
+
instance_variable_set("@#{attr}_cached_loaded", true)
|
|
360
|
+
end
|
|
361
|
+
return
|
|
362
|
+
end
|
|
363
|
+
end
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
# Check if this record was filtered out by where_rpc conditions
|
|
367
|
+
if instance_variable_get(:@_rpc_data_filtered_out)
|
|
368
|
+
# Set empty/default values to avoid repeated attempts
|
|
369
|
+
instance_variable_set("@#{options[:method_name]}", nil)
|
|
370
|
+
instance_variable_set("@#{options[:method_name]}_loaded", true)
|
|
371
|
+
|
|
372
|
+
# Cache default values for attributes
|
|
373
|
+
attributes.each do |attr|
|
|
374
|
+
default_value = config.attribute_defaults[attr]
|
|
375
|
+
instance_variable_set("@#{attr}_cached", default_value)
|
|
376
|
+
instance_variable_set("@#{attr}_cached_loaded", true)
|
|
377
|
+
end
|
|
378
|
+
return
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
# Check if we're in a where_rpc context and should skip individual loading
|
|
382
|
+
if Thread.current[:active_rpc_where_rpc_context]
|
|
383
|
+
# Set empty/default values to avoid repeated attempts
|
|
384
|
+
instance_variable_set("@#{options[:method_name]}", nil)
|
|
385
|
+
instance_variable_set("@#{options[:method_name]}_loaded", true)
|
|
386
|
+
|
|
387
|
+
# Cache default values for attributes
|
|
388
|
+
attributes.each do |attr|
|
|
389
|
+
default_value = config.attribute_defaults[attr]
|
|
390
|
+
instance_variable_set("@#{attr}_cached", default_value)
|
|
391
|
+
instance_variable_set("@#{attr}_cached_loaded", true)
|
|
392
|
+
end
|
|
393
|
+
return
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
send("load_#{options[:method_name]}")
|
|
397
|
+
end
|
|
398
|
+
|
|
399
|
+
# Define method to fetch data from the gRPC service
|
|
400
|
+
define_method("fetch_#{options[:method_name]}_from_#{service}") do
|
|
401
|
+
foreign_key_value = send(options[:foreign_key])
|
|
402
|
+
|
|
403
|
+
# Check if local mode is enabled - skip gRPC calls entirely
|
|
404
|
+
if ActiveRpc.local_mode?
|
|
405
|
+
Rails.logger.debug("[ActiveRpc Local Mode] Skipping gRPC call for #{resource} with #{options[:foreign_key]}=#{foreign_key_value}")
|
|
406
|
+
|
|
407
|
+
# Create a mock response with default values
|
|
408
|
+
message_data = { id: foreign_key_value }
|
|
409
|
+
config.attributes.each do |attr|
|
|
410
|
+
message_data[attr] = config.attribute_defaults[attr] if config.attribute_defaults.key?(attr)
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
return OpenStruct.new(message: OpenStruct.new(message_data))
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
# Add nil check before calling methods on the client
|
|
417
|
+
if self.class.grpc_client.nil?
|
|
418
|
+
Rails.logger.error("Failed to fetch #{resource} data for #{options[:foreign_key]}=#{send(options[:foreign_key])}: gRPC client is nil")
|
|
419
|
+
return nil
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
formatted_id = ActiveRpc::ModelExtensions.format_foreign_key_value(self.class, options[:foreign_key],
|
|
423
|
+
foreign_key_value)
|
|
424
|
+
response = self.class.grpc_call_with_locale(:"Get#{resource}", id: formatted_id)
|
|
425
|
+
|
|
426
|
+
# Add nil check for the response
|
|
427
|
+
if response.nil?
|
|
428
|
+
Rails.logger.error("Failed to fetch #{resource} data for #{options[:foreign_key]}=#{send(options[:foreign_key])}: gRPC response is nil")
|
|
429
|
+
return nil
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
response
|
|
433
|
+
rescue StandardError => e
|
|
434
|
+
Rails.logger.error("Failed to fetch #{resource} data for #{options[:foreign_key]}=#{send(options[:foreign_key])}: #{e.message}")
|
|
435
|
+
nil
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
# NOTE: Bulk method is defined later with query_options support (line ~1083)
|
|
439
|
+
|
|
440
|
+
# Define a scope for eager loading gRPC data
|
|
441
|
+
scope_method_name = "with_#{resource.to_s.underscore}_data".to_sym
|
|
442
|
+
define_singleton_method(scope_method_name) do |query_options = {}|
|
|
443
|
+
# Return a relation that can be chained, but mark it for bulk loading
|
|
444
|
+
relation = all
|
|
445
|
+
|
|
446
|
+
# Store the bulk loading configuration in the relation
|
|
447
|
+
relation.instance_variable_set(:@_bulk_loading_enabled, true)
|
|
448
|
+
relation.instance_variable_set(:@_bulk_loading_resource, resource)
|
|
449
|
+
# Include resource in options for generic access
|
|
450
|
+
bulk_options = options.merge(resource: resource)
|
|
451
|
+
relation.instance_variable_set(:@_bulk_loading_options, bulk_options)
|
|
452
|
+
relation.instance_variable_set(:@_bulk_loading_query_options, query_options)
|
|
453
|
+
relation.instance_variable_set(:@_bulk_loading_attributes, attributes)
|
|
454
|
+
|
|
455
|
+
# IDs will be extracted when bulk loading is first triggered
|
|
456
|
+
|
|
457
|
+
# Create a unique cache key for this relation to avoid collisions between different relations
|
|
458
|
+
@@bulk_loading_counter ||= 0
|
|
459
|
+
@@bulk_loading_counter += 1
|
|
460
|
+
cache_key = "bulk_loading_#{@@bulk_loading_counter}"
|
|
461
|
+
relation.instance_variable_set(:@_bulk_loading_cache_key, cache_key)
|
|
462
|
+
|
|
463
|
+
# Create a unique cache key for this relation to avoid collisions
|
|
464
|
+
cache_key = "bulk_loading_#{relation.object_id}"
|
|
465
|
+
relation.instance_variable_set(:@_bulk_loading_cache_key, cache_key)
|
|
466
|
+
|
|
467
|
+
# Create a unique cache key for this specific relation
|
|
468
|
+
cache_key = "bulk_loading_#{relation.object_id}_#{Time.now.to_f}"
|
|
469
|
+
relation.instance_variable_set(:@_bulk_loading_cache_key, cache_key)
|
|
470
|
+
|
|
471
|
+
# Method to add RPC filtering conditions using Ransack-style queries
|
|
472
|
+
# This sends filters to the gRPC service's 'q' parameter
|
|
473
|
+
relation.define_singleton_method(:filter_rpc) do |conditions = {}|
|
|
474
|
+
Rails.logger.debug("[ActiveRpc] filter_rpc called with conditions: #{conditions.keys}") if defined?(Rails)
|
|
475
|
+
|
|
476
|
+
# Store the conditions for later processing as Ransack queries
|
|
477
|
+
@_filter_rpc_conditions = (@_filter_rpc_conditions || {}).merge(conditions)
|
|
478
|
+
|
|
479
|
+
# Return self to allow chaining
|
|
480
|
+
self
|
|
481
|
+
end
|
|
482
|
+
|
|
483
|
+
# Method to call remote scopes for complex queries
|
|
484
|
+
# This sends scope calls to the gRPC service's 'scope' parameter
|
|
485
|
+
relation.define_singleton_method(:rpc_scope) do |scope_name, value = nil, **args|
|
|
486
|
+
if defined?(Rails)
|
|
487
|
+
Rails.logger.debug("[ActiveRpc] rpc_scope called: #{scope_name} with value: #{value}, args: #{args}")
|
|
488
|
+
end
|
|
489
|
+
|
|
490
|
+
# Store the scope calls for later processing
|
|
491
|
+
# If value is provided, use it directly; otherwise use the first arg value
|
|
492
|
+
scope_value = value || (args.any? ? args.values.first : nil)
|
|
493
|
+
@_rpc_scope_conditions = (@_rpc_scope_conditions || {}).merge(scope_name => scope_value)
|
|
494
|
+
|
|
495
|
+
# Return self to allow chaining
|
|
496
|
+
self
|
|
497
|
+
end
|
|
498
|
+
|
|
499
|
+
# Helper method to check if we have any RPC conditions
|
|
500
|
+
relation.define_singleton_method(:has_rpc_conditions?) do
|
|
501
|
+
@_filter_rpc_conditions.present? || @_rpc_scope_conditions.present?
|
|
502
|
+
end
|
|
503
|
+
|
|
504
|
+
# Helper method to apply RPC filters and call the original method
|
|
505
|
+
relation.define_singleton_method(:apply_rpc_filters_then_call) do |method_name, *args, &block|
|
|
506
|
+
Rails.logger.debug("[ActiveRpc] apply_rpc_filters_then_call called for #{method_name}") if defined?(Rails)
|
|
507
|
+
|
|
508
|
+
# Check if we have any RPC filtering conditions to process
|
|
509
|
+
has_conditions = has_rpc_conditions?
|
|
510
|
+
|
|
511
|
+
if has_conditions
|
|
512
|
+
Rails.logger.debug('[ActiveRpc] Making gRPC call with RPC conditions') if defined?(Rails)
|
|
513
|
+
|
|
514
|
+
# Determine bulk method based on resource type
|
|
515
|
+
resource = @_bulk_loading_options[:resource] || 'User'
|
|
516
|
+
default_bulk_method = "list_#{resource.to_s.pluralize.underscore}".to_sym
|
|
517
|
+
bulk_method = @_bulk_loading_options[:bulk_method] || default_bulk_method
|
|
518
|
+
|
|
519
|
+
# Prepare query options for different types of RPC calls
|
|
520
|
+
query_options = {}
|
|
521
|
+
|
|
522
|
+
# 1. Handle filter_rpc conditions (Ransack queries)
|
|
523
|
+
if @_filter_rpc_conditions.present?
|
|
524
|
+
if defined?(Rails)
|
|
525
|
+
Rails.logger.debug("[ActiveRpc] Processing filter_rpc conditions: #{@_filter_rpc_conditions.inspect}")
|
|
526
|
+
end
|
|
527
|
+
query_options[:q] = @_filter_rpc_conditions
|
|
528
|
+
end
|
|
529
|
+
|
|
530
|
+
# 2. Handle rpc_scope conditions (remote scopes)
|
|
531
|
+
if @_rpc_scope_conditions.present?
|
|
532
|
+
if defined?(Rails)
|
|
533
|
+
Rails.logger.debug("[ActiveRpc] Processing rpc_scope conditions: #{@_rpc_scope_conditions.inspect}")
|
|
534
|
+
end
|
|
535
|
+
existing_scopes = query_options[:scope] || {}
|
|
536
|
+
query_options[:scope] = existing_scopes.merge(@_rpc_scope_conditions)
|
|
537
|
+
end
|
|
538
|
+
|
|
539
|
+
Rails.logger.debug("[ActiveRpc] Final query_options: #{query_options.inspect}") if defined?(Rails)
|
|
540
|
+
|
|
541
|
+
filtered_data = klass.send(bulk_method, [], query_options)
|
|
542
|
+
filtered_ids = filtered_data.map(&:id)
|
|
543
|
+
resource_name = @_bulk_loading_options[:resource] || 'records'
|
|
544
|
+
if defined?(Rails)
|
|
545
|
+
Rails.logger.debug("[ActiveRpc] Got #{filtered_data.size} #{resource_name.to_s.downcase} from gRPC call")
|
|
546
|
+
end
|
|
547
|
+
|
|
548
|
+
# 2. Handle empty results
|
|
549
|
+
if filtered_ids.empty?
|
|
550
|
+
Rails.logger.debug('[ActiveRpc] No filtered results, returning empty relation') if defined?(Rails)
|
|
551
|
+
# For count method, return 0 directly to avoid infinite recursion
|
|
552
|
+
return 0 if method_name == :count
|
|
553
|
+
|
|
554
|
+
# For other methods, create empty relation from current relation to preserve ActiveRpc overrides
|
|
555
|
+
empty_relation = none
|
|
556
|
+
# Clear RPC conditions to prevent infinite recursion
|
|
557
|
+
empty_relation.instance_variable_set(:@_filter_rpc_conditions, {})
|
|
558
|
+
empty_relation.instance_variable_set(:@_rpc_scope_conditions, {})
|
|
559
|
+
return empty_relation.send(method_name, *args, &block) if block_given?
|
|
560
|
+
|
|
561
|
+
return empty_relation.send(method_name, *args)
|
|
562
|
+
|
|
563
|
+
end
|
|
564
|
+
|
|
565
|
+
# 3. Create new relation with filtered IDs
|
|
566
|
+
foreign_key = @_bulk_loading_options[:foreign_key]
|
|
567
|
+
if defined?(Rails)
|
|
568
|
+
Rails.logger.debug("[ActiveRpc] Creating new relation with #{foreign_key} IN #{filtered_ids.size} IDs")
|
|
569
|
+
end
|
|
570
|
+
new_relation = where(foreign_key => filtered_ids)
|
|
571
|
+
|
|
572
|
+
# 4. Clear all RPC conditions to prevent infinite recursion
|
|
573
|
+
# Keep the bulk_loading_options so the relation retains its ActiveRpc configuration
|
|
574
|
+
new_relation.instance_variable_set(:@_filter_rpc_conditions, {})
|
|
575
|
+
new_relation.instance_variable_set(:@_rpc_scope_conditions, {})
|
|
576
|
+
|
|
577
|
+
# 5. Call the original method on the new relation with proper argument handling
|
|
578
|
+
result = if block_given?
|
|
579
|
+
new_relation.send(method_name, *args, &block)
|
|
580
|
+
else
|
|
581
|
+
new_relation.send(method_name, *args)
|
|
582
|
+
end
|
|
583
|
+
Rails.logger.debug("[ActiveRpc] #{method_name} completed with result: #{result.class}") if defined?(Rails)
|
|
584
|
+
result
|
|
585
|
+
elsif block_given?
|
|
586
|
+
# No RPC conditions, use normal behavior with proper argument handling
|
|
587
|
+
super(*args, &block)
|
|
588
|
+
else
|
|
589
|
+
super(*args)
|
|
590
|
+
end
|
|
591
|
+
end
|
|
592
|
+
|
|
593
|
+
# Override enumeration methods to use new RPC filter pattern
|
|
594
|
+
relation.define_singleton_method(:each) do |&block|
|
|
595
|
+
if has_rpc_conditions?
|
|
596
|
+
apply_rpc_filters_then_call(:each, &block)
|
|
597
|
+
else
|
|
598
|
+
super(&block)
|
|
599
|
+
end
|
|
600
|
+
end
|
|
601
|
+
|
|
602
|
+
relation.define_singleton_method(:map) do |&block|
|
|
603
|
+
if has_rpc_conditions?
|
|
604
|
+
apply_rpc_filters_then_call(:map, &block)
|
|
605
|
+
else
|
|
606
|
+
super(&block)
|
|
607
|
+
end
|
|
608
|
+
end
|
|
609
|
+
|
|
610
|
+
relation.define_singleton_method(:to_a) do
|
|
611
|
+
if has_rpc_conditions?
|
|
612
|
+
apply_rpc_filters_then_call(:to_a)
|
|
613
|
+
else
|
|
614
|
+
super()
|
|
615
|
+
end
|
|
616
|
+
end
|
|
617
|
+
|
|
618
|
+
# Override individual record access methods to use new RPC filter pattern
|
|
619
|
+
relation.define_singleton_method(:first) do |limit = nil|
|
|
620
|
+
if has_rpc_conditions?
|
|
621
|
+
apply_rpc_filters_then_call(:first, limit)
|
|
622
|
+
else
|
|
623
|
+
super(limit)
|
|
624
|
+
end
|
|
625
|
+
end
|
|
626
|
+
|
|
627
|
+
relation.define_singleton_method(:second) do
|
|
628
|
+
if has_rpc_conditions?
|
|
629
|
+
apply_rpc_filters_then_call(:second)
|
|
630
|
+
else
|
|
631
|
+
super()
|
|
632
|
+
end
|
|
633
|
+
end
|
|
634
|
+
|
|
635
|
+
relation.define_singleton_method(:last) do |limit = nil|
|
|
636
|
+
if has_rpc_conditions?
|
|
637
|
+
apply_rpc_filters_then_call(:last, limit)
|
|
638
|
+
else
|
|
639
|
+
super(limit)
|
|
640
|
+
end
|
|
641
|
+
end
|
|
642
|
+
|
|
643
|
+
relation.define_singleton_method(:[]) do |index|
|
|
644
|
+
if has_rpc_conditions?
|
|
645
|
+
apply_rpc_filters_then_call(:[], index)
|
|
646
|
+
else
|
|
647
|
+
super(index)
|
|
648
|
+
end
|
|
649
|
+
end
|
|
650
|
+
|
|
651
|
+
# Override evaluation methods to use new RPC filter pattern
|
|
652
|
+
relation.define_singleton_method(:count) do |column_name = nil|
|
|
653
|
+
if has_rpc_conditions?
|
|
654
|
+
Rails.logger.debug('[ActiveRpc] Using RPC filter pattern for count') if defined?(Rails)
|
|
655
|
+
apply_rpc_filters_then_call(:count, column_name)
|
|
656
|
+
else
|
|
657
|
+
# Use the original ActiveRecord count method directly
|
|
658
|
+
ActiveRecord::Relation.instance_method(:count).bind_call(self, column_name)
|
|
659
|
+
end
|
|
660
|
+
end
|
|
661
|
+
|
|
662
|
+
relation.define_singleton_method(:size) do
|
|
663
|
+
if has_rpc_conditions?
|
|
664
|
+
apply_rpc_filters_then_call(:size)
|
|
665
|
+
else
|
|
666
|
+
super()
|
|
667
|
+
end
|
|
668
|
+
end
|
|
669
|
+
|
|
670
|
+
relation.define_singleton_method(:inspect) do
|
|
671
|
+
if has_rpc_conditions?
|
|
672
|
+
apply_rpc_filters_then_call(:inspect)
|
|
673
|
+
else
|
|
674
|
+
super()
|
|
675
|
+
end
|
|
676
|
+
end
|
|
677
|
+
|
|
678
|
+
relation.define_singleton_method(:length) do
|
|
679
|
+
if has_rpc_conditions?
|
|
680
|
+
apply_rpc_filters_then_call(:length)
|
|
681
|
+
else
|
|
682
|
+
super()
|
|
683
|
+
end
|
|
684
|
+
end
|
|
685
|
+
|
|
686
|
+
relation.define_singleton_method(:empty?) do
|
|
687
|
+
if has_rpc_conditions?
|
|
688
|
+
apply_rpc_filters_then_call(:empty?)
|
|
689
|
+
else
|
|
690
|
+
super()
|
|
691
|
+
end
|
|
692
|
+
end
|
|
693
|
+
|
|
694
|
+
# NOTE: Old post-filtering method removed - using new RPC filter pattern
|
|
695
|
+
|
|
696
|
+
# Define the bulk loading method
|
|
697
|
+
relation.define_singleton_method(:perform_bulk_loading_if_needed) do
|
|
698
|
+
return if @_bulk_loading_performed
|
|
699
|
+
|
|
700
|
+
# Extract IDs from the current relation state and store for reuse
|
|
701
|
+
bulk_options = @_bulk_loading_options
|
|
702
|
+
|
|
703
|
+
# Check if we have where_rpc conditions that should be applied remotely
|
|
704
|
+
has_remote_conditions = @_bulk_loading_query_options&.dig(:where)&.present? ||
|
|
705
|
+
@_bulk_loading_query_options&.dig(:scope)&.present?
|
|
706
|
+
|
|
707
|
+
Rails.logger.debug("[ActiveRpc] has_remote_conditions: #{has_remote_conditions}") if defined?(Rails)
|
|
708
|
+
Rails.logger.debug("[ActiveRpc] query_options: #{@_bulk_loading_query_options.keys}") if defined?(Rails)
|
|
709
|
+
|
|
710
|
+
if has_remote_conditions
|
|
711
|
+
# For remote filtering, don't extract IDs - let the remote service filter
|
|
712
|
+
Rails.logger.debug('[ActiveRpc] Using remote filtering - not extracting IDs') if defined?(Rails)
|
|
713
|
+
ids = []
|
|
714
|
+
else
|
|
715
|
+
# For regular bulk loading, extract IDs as usual
|
|
716
|
+
if @_bulk_loading_extracted_ids
|
|
717
|
+
ids = @_bulk_loading_extracted_ids
|
|
718
|
+
else
|
|
719
|
+
ids = pluck(bulk_options[:foreign_key]).compact.uniq
|
|
720
|
+
@_bulk_loading_extracted_ids = ids
|
|
721
|
+
end
|
|
722
|
+
|
|
723
|
+
# Skip if no IDs and no remote conditions
|
|
724
|
+
return if ids.empty?
|
|
725
|
+
end
|
|
726
|
+
|
|
727
|
+
# Call the bulk method
|
|
728
|
+
bulk_method = bulk_options[:bulk_method]
|
|
729
|
+
|
|
730
|
+
# Get the model class to call the bulk method
|
|
731
|
+
model_class = klass
|
|
732
|
+
bulk_data = model_class.send(bulk_method, ids, @_bulk_loading_query_options)
|
|
733
|
+
|
|
734
|
+
# Create lookup hash from bulk data
|
|
735
|
+
lookup = {}
|
|
736
|
+
bulk_data.each do |item|
|
|
737
|
+
lookup[item.id.to_s] = item if item.respond_to?(:id)
|
|
738
|
+
end
|
|
739
|
+
|
|
740
|
+
# Store the lookup using a hash-based cache to avoid collisions
|
|
741
|
+
cache_key = @_bulk_loading_cache_key
|
|
742
|
+
|
|
743
|
+
# Initialize the cache hash if it doesn't exist
|
|
744
|
+
cache_hash = model_class.instance_variable_get(:@_bulk_loading_cache) || {}
|
|
745
|
+
cache_hash[cache_key] = {
|
|
746
|
+
lookup: lookup,
|
|
747
|
+
attributes: @_bulk_loading_attributes,
|
|
748
|
+
options: bulk_options
|
|
749
|
+
}
|
|
750
|
+
model_class.instance_variable_set(:@_bulk_loading_cache, cache_hash)
|
|
751
|
+
|
|
752
|
+
# Store the current cache key in a thread-local variable so after_find can access it
|
|
753
|
+
Thread.current[:active_rpc_current_cache_key] = cache_key
|
|
754
|
+
|
|
755
|
+
# Mark as performed to avoid duplicate calls
|
|
756
|
+
@_bulk_loading_performed = true
|
|
757
|
+
end
|
|
758
|
+
relation
|
|
759
|
+
end
|
|
760
|
+
|
|
761
|
+
# Define a method to directly access the gRPC client for custom calls
|
|
762
|
+
client_method_name = "#{resource.to_s.underscore}_client".to_sym
|
|
763
|
+
define_singleton_method(client_method_name) do
|
|
764
|
+
grpc_client
|
|
765
|
+
end
|
|
766
|
+
|
|
767
|
+
# We don't need a separate method to check service availability
|
|
768
|
+
# Instead, we'll handle connection errors directly in the create/update methods
|
|
769
|
+
|
|
770
|
+
# Define a class method for creation (for backward compatibility with tests)
|
|
771
|
+
define_singleton_method("create_#{resource.to_s.underscore}") do |attributes = {}|
|
|
772
|
+
# Create a new instance
|
|
773
|
+
instance = new
|
|
774
|
+
|
|
775
|
+
# Build the resource
|
|
776
|
+
instance.send("build_#{resource.to_s.underscore}", attributes)
|
|
777
|
+
|
|
778
|
+
# Create and associate the resource
|
|
779
|
+
return unless instance.send("create_and_associate_#{resource.to_s.underscore}", attributes)
|
|
780
|
+
|
|
781
|
+
# Return the ID
|
|
782
|
+
instance.send(options[:foreign_key])
|
|
783
|
+
end
|
|
784
|
+
|
|
785
|
+
# Define method to build a new resource without saving it
|
|
786
|
+
define_method("build_#{resource.to_s.underscore}") do |attributes = {}|
|
|
787
|
+
# Set any attributes that match our delegated attributes
|
|
788
|
+
attributes.each do |attr, value|
|
|
789
|
+
attr_sym = attr.to_sym
|
|
790
|
+
|
|
791
|
+
# Check if this is a remote attribute
|
|
792
|
+
next unless self.class.active_rpc_config[:attributes].include?(attr_sym)
|
|
793
|
+
|
|
794
|
+
# Track the change using ActiveModel::Dirty
|
|
795
|
+
send("#{attr}_will_change!") if respond_to?("#{attr}_will_change!")
|
|
796
|
+
|
|
797
|
+
# Set the attribute value
|
|
798
|
+
send("#{attr}=", value)
|
|
799
|
+
end
|
|
800
|
+
|
|
801
|
+
# Return self for method chaining
|
|
802
|
+
self
|
|
803
|
+
end
|
|
804
|
+
|
|
805
|
+
# Define an instance method to make custom RPC calls
|
|
806
|
+
custom_call_method = "call_#{resource.to_s.underscore}_rpc".to_sym
|
|
807
|
+
define_method(custom_call_method) do |method_name, params = {}|
|
|
808
|
+
# Add field tracking metadata for custom RPC calls with parameters
|
|
809
|
+
if params.is_a?(Hash) && !params.empty?
|
|
810
|
+
params_with_fields = params.to_h
|
|
811
|
+
params_with_fields[:_fields] = ActiveRpc::ModelExtensions._active_rpc_extract_field_paths(params)
|
|
812
|
+
self.class.grpc_call_with_locale(method_name, params_with_fields)
|
|
813
|
+
else
|
|
814
|
+
self.class.grpc_call_with_locale(method_name, params)
|
|
815
|
+
end
|
|
816
|
+
rescue StandardError => e
|
|
817
|
+
Rails.logger.error("Failed to call custom RPC method #{method_name} with params #{params}: #{e.message}")
|
|
818
|
+
nil
|
|
819
|
+
end
|
|
820
|
+
|
|
821
|
+
# Define method to save pending updates to the gRPC service
|
|
822
|
+
update_method = "update_#{resource.to_s.underscore}".to_sym
|
|
823
|
+
define_method(update_method) do
|
|
824
|
+
# Check if there are any changed attributes
|
|
825
|
+
if send("has_#{resource.to_s.underscore}_updates?")
|
|
826
|
+
foreign_key_value = send(options[:foreign_key])
|
|
827
|
+
|
|
828
|
+
# Return false if foreign key is not present
|
|
829
|
+
unless foreign_key_value.present?
|
|
830
|
+
Rails.logger.error("[ActiveRpc] Cannot update #{resource} without a valid #{options[:foreign_key]}")
|
|
831
|
+
errors.add(options[:foreign_key], "Cannot update #{resource.to_s.underscore} without a valid ID")
|
|
832
|
+
return false
|
|
833
|
+
end
|
|
834
|
+
|
|
835
|
+
# Prepare parameters for the update call
|
|
836
|
+
formatted_id = ActiveRpc::ModelExtensions.format_foreign_key_value(self.class, options[:foreign_key],
|
|
837
|
+
foreign_key_value)
|
|
838
|
+
params = { id: formatted_id }
|
|
839
|
+
|
|
840
|
+
# DEBUG: Log the formatted ID
|
|
841
|
+
Rails.logger.debug("[ActiveRpc DEBUG] Foreign key #{options[:foreign_key]}: #{foreign_key_value.inspect} (#{foreign_key_value.class}) -> formatted: #{formatted_id.inspect} (#{formatted_id.class})")
|
|
842
|
+
|
|
843
|
+
# Add all changed attributes to the params
|
|
844
|
+
pending_updates = send("pending_#{resource.to_s.underscore}_updates")
|
|
845
|
+
params.merge!(pending_updates)
|
|
846
|
+
|
|
847
|
+
Rails.logger.info("[ActiveRpc] Updating #{resource} with ID #{foreign_key_value} with params: #{params.keys}")
|
|
848
|
+
|
|
849
|
+
# Call the UpdateUser method on the gRPC service
|
|
850
|
+
begin
|
|
851
|
+
# Add field tracking metadata to help with protobuf field presence
|
|
852
|
+
params_with_fields = params.to_h
|
|
853
|
+
params_with_fields[:_fields] = ActiveRpc::ModelExtensions._active_rpc_extract_field_paths(params)
|
|
854
|
+
|
|
855
|
+
response = self.class.grpc_call_with_locale("Update#{resource}".to_sym, params_with_fields)
|
|
856
|
+
|
|
857
|
+
# Check if the update was successful by looking at _metadata first
|
|
858
|
+
if response&.message&.respond_to?(:_metadata) && response.message._metadata&.success == false
|
|
859
|
+
# Handle validation errors from _metadata
|
|
860
|
+
if response.message._metadata.errors && response.message._metadata.errors.length > 0
|
|
861
|
+
response.message._metadata.errors.each do |field, error_messages|
|
|
862
|
+
messages =
|
|
863
|
+
if error_messages.respond_to?(:messages)
|
|
864
|
+
error_messages.messages
|
|
865
|
+
elsif error_messages.is_a?(Array)
|
|
866
|
+
error_messages
|
|
867
|
+
else
|
|
868
|
+
Array(error_messages)
|
|
869
|
+
end
|
|
870
|
+
|
|
871
|
+
messages.each { |message| errors.add(field.to_sym, message) }
|
|
872
|
+
end
|
|
873
|
+
else
|
|
874
|
+
errors.add(:base, 'Validation failed')
|
|
875
|
+
end
|
|
876
|
+
false
|
|
877
|
+
elsif response&.message
|
|
878
|
+
# Update attributes with the response data
|
|
879
|
+
if response.message.respond_to?(:to_h)
|
|
880
|
+
# Convert the response message to a hash
|
|
881
|
+
response_data = response.message.to_h
|
|
882
|
+
|
|
883
|
+
# Use intersection to get only the attributes that exist in both the response and our model
|
|
884
|
+
attrs_to_assign = response_data.slice(*self.class.active_rpc_config[:attributes].map(&:to_sym))
|
|
885
|
+
|
|
886
|
+
# Assign the attributes in one go
|
|
887
|
+
assign_attributes(attrs_to_assign)
|
|
888
|
+
end
|
|
889
|
+
|
|
890
|
+
clear_attribute_changes(rpc_attribute_names) if respond_to?(:clear_attribute_changes)
|
|
891
|
+
|
|
892
|
+
Rails.logger.info("[ActiveRpc] Successfully updated #{resource} with ID #{foreign_key_value}")
|
|
893
|
+
true
|
|
894
|
+
else
|
|
895
|
+
# Handle server error response
|
|
896
|
+
error_message = 'No response message'
|
|
897
|
+
|
|
898
|
+
# Extract error details if available
|
|
899
|
+
if response&.error
|
|
900
|
+
error_code = response.error.respond_to?(:code) ? response.error.code : 'UNKNOWN'
|
|
901
|
+
error_details = response.error.respond_to?(:message) ? response.error.message : 'Unknown error'
|
|
902
|
+
error_message = "Server returned error: #{error_code} - #{error_details}"
|
|
903
|
+
|
|
904
|
+
# Add specific error messages to the model
|
|
905
|
+
# Clean up the error message - gRPC adds some debug info
|
|
906
|
+
clean_error = error_details.gsub(/\. debug_error_string:.+$/, '')
|
|
907
|
+
|
|
908
|
+
# Check if the error response has structured validation_errors
|
|
909
|
+
if response.error.respond_to?(:validation_errors) && response.error.validation_errors.present?
|
|
910
|
+
# Get the validation errors
|
|
911
|
+
validation_errors = response.error.validation_errors
|
|
912
|
+
|
|
913
|
+
# Add each validation error to the model
|
|
914
|
+
validation_errors.each do |field, messages|
|
|
915
|
+
field_sym = field.to_sym
|
|
916
|
+
|
|
917
|
+
# Convert messages to array if it's not already
|
|
918
|
+
messages_array = messages.is_a?(Array) ? messages : [ messages ]
|
|
919
|
+
|
|
920
|
+
# Add each message to the field
|
|
921
|
+
messages_array.each do |message|
|
|
922
|
+
errors.add(field_sym, message)
|
|
923
|
+
end
|
|
924
|
+
end
|
|
925
|
+
# Check if the error response has full_messages
|
|
926
|
+
elsif response.error.respond_to?(:full_messages) && response.error.full_messages.present?
|
|
927
|
+
# Add each full message to the model
|
|
928
|
+
response.error.full_messages.each do |message|
|
|
929
|
+
# Try to parse the message to extract field and error
|
|
930
|
+
if message =~ /^([A-Za-z_]+) (is .+)$/
|
|
931
|
+
field = ::Regexp.last_match(1).downcase.to_sym
|
|
932
|
+
error_msg = ::Regexp.last_match(2)
|
|
933
|
+
errors.add(field, error_msg)
|
|
934
|
+
else
|
|
935
|
+
errors.add(options[:foreign_key], message)
|
|
936
|
+
end
|
|
937
|
+
end
|
|
938
|
+
# Check for validation errors in the error message
|
|
939
|
+
elsif clean_error.include?('Validation failed:')
|
|
940
|
+
# Extract the validation errors part
|
|
941
|
+
validation_part = clean_error.gsub(/^\d+:/, '').strip
|
|
942
|
+
validation_errors = validation_part.gsub('Validation failed: ', '').split(', ')
|
|
943
|
+
|
|
944
|
+
validation_errors.each do |error|
|
|
945
|
+
# Parse errors in format "Field is invalid"
|
|
946
|
+
if error =~ /^([A-Za-z_]+) (is .+)$/
|
|
947
|
+
field = ::Regexp.last_match(1).downcase.to_sym
|
|
948
|
+
message = ::Regexp.last_match(2)
|
|
949
|
+
errors.add(field, message)
|
|
950
|
+
elsif error.include?(':')
|
|
951
|
+
field, message = error.split(':', 2)
|
|
952
|
+
errors.add(field.strip.to_sym, message.strip)
|
|
953
|
+
else
|
|
954
|
+
errors.add(options[:foreign_key], error.strip)
|
|
955
|
+
end
|
|
956
|
+
end
|
|
957
|
+
else
|
|
958
|
+
errors.add(options[:foreign_key],
|
|
959
|
+
"Failed to update #{resource.to_s.underscore} record: #{clean_error}")
|
|
960
|
+
end
|
|
961
|
+
else
|
|
962
|
+
errors.add(options[:foreign_key], "Failed to update #{resource.to_s.underscore} record")
|
|
963
|
+
end
|
|
964
|
+
|
|
965
|
+
Rails.logger.error("[ActiveRpc] Failed to update #{resource} with ID #{foreign_key_value}: #{error_message}")
|
|
966
|
+
false
|
|
967
|
+
end
|
|
968
|
+
rescue StandardError => e
|
|
969
|
+
# Handle any exceptions that might occur during the RPC call
|
|
970
|
+
error_message = e.message
|
|
971
|
+
|
|
972
|
+
# Check if this is a connection error
|
|
973
|
+
if error_message.include?('Connection refused') ||
|
|
974
|
+
error_message.include?('failed to connect') ||
|
|
975
|
+
error_message.include?('Deadline exceeded') ||
|
|
976
|
+
error_message.include?('UNAVAILABLE')
|
|
977
|
+
# This is a connection error, so the service is likely down
|
|
978
|
+
Rails.logger.error("[ActiveRpc] Failed to connect to #{resource} service: #{error_message}")
|
|
979
|
+
errors.add(options[:foreign_key], 'Unable to connect to the required service. Please try again later.')
|
|
980
|
+
else
|
|
981
|
+
# This is some other error
|
|
982
|
+
Rails.logger.error("[ActiveRpc] Error updating #{resource}: #{error_message}")
|
|
983
|
+
errors.add(options[:foreign_key],
|
|
984
|
+
"Failed to update #{resource.to_s.underscore} record: #{error_message}")
|
|
985
|
+
end
|
|
986
|
+
|
|
987
|
+
false
|
|
988
|
+
end
|
|
989
|
+
else
|
|
990
|
+
# No pending updates, return true
|
|
991
|
+
Rails.logger.debug("[ActiveRpc] No pending updates for #{resource} with ID #{send(options[:foreign_key])}")
|
|
992
|
+
true
|
|
993
|
+
end
|
|
994
|
+
end
|
|
995
|
+
|
|
996
|
+
# Define save method for the resource
|
|
997
|
+
define_method("save_#{resource.to_s.underscore}") do
|
|
998
|
+
if send(options[:foreign_key]).present?
|
|
999
|
+
# Update existing resource
|
|
1000
|
+
send(update_method)
|
|
1001
|
+
elsif attributes.any? { |attr| changed_attributes.key?(attr.to_s) }
|
|
1002
|
+
# Create new resource
|
|
1003
|
+
create_attrs = {}
|
|
1004
|
+
attributes.each do |attr|
|
|
1005
|
+
next unless changed_attributes.key?(attr.to_s)
|
|
1006
|
+
|
|
1007
|
+
type = if self.class.respond_to?(:attribute_types)
|
|
1008
|
+
self.class.attribute_types[attr.to_s]
|
|
1009
|
+
end
|
|
1010
|
+
create_attrs[attr] = ActiveRpc::ModelExtensions.prepare_value_for_rpc(send(attr), type)
|
|
1011
|
+
end
|
|
1012
|
+
|
|
1013
|
+
# Create and associate the resource
|
|
1014
|
+
send("create_and_associate_#{resource.to_s.underscore}", create_attrs)
|
|
1015
|
+
# Convert changed attributes to a hash for create
|
|
1016
|
+
else
|
|
1017
|
+
# No attributes to save
|
|
1018
|
+
true
|
|
1019
|
+
end
|
|
1020
|
+
end
|
|
1021
|
+
|
|
1022
|
+
# Define save! method for the resource
|
|
1023
|
+
define_method("save_#{resource.to_s.underscore}!") do
|
|
1024
|
+
result = send("save_#{resource.to_s.underscore}")
|
|
1025
|
+
raise "Failed to save #{resource.to_s.underscore}: #{errors.full_messages.join(', ')}" unless result
|
|
1026
|
+
|
|
1027
|
+
result
|
|
1028
|
+
end
|
|
1029
|
+
|
|
1030
|
+
# Define method to check if there are pending updates
|
|
1031
|
+
define_method("has_#{resource.to_s.underscore}_updates?".to_sym) do
|
|
1032
|
+
# Check if any of the remote attributes have changed using ActiveModel::Dirty
|
|
1033
|
+
# Use changed_attributes to avoid infinite recursion
|
|
1034
|
+
attributes.any? { |attr| changed_attributes.key?(attr.to_s) }
|
|
1035
|
+
end
|
|
1036
|
+
|
|
1037
|
+
# Define method to get changes to delegated attributes
|
|
1038
|
+
define_method("#{resource.to_s.underscore}_changes".to_sym) do
|
|
1039
|
+
changes.slice(*attributes.map(&:to_s))
|
|
1040
|
+
end
|
|
1041
|
+
|
|
1042
|
+
# Define method to clear pending updates without saving them
|
|
1043
|
+
define_method("discard_#{resource.to_s.underscore}_updates".to_sym) do
|
|
1044
|
+
if send("has_#{resource.to_s.underscore}_updates?")
|
|
1045
|
+
# Get the original values from the server
|
|
1046
|
+
send(options[:reload_method])
|
|
1047
|
+
|
|
1048
|
+
# Clear remote attribute changes without touching local ActiveRecord fields
|
|
1049
|
+
clear_attribute_changes(rpc_attribute_names) if respond_to?(:clear_attribute_changes)
|
|
1050
|
+
|
|
1051
|
+
Rails.logger.info("[ActiveRpc] Discarded pending updates for #{resource} with ID #{send(options[:foreign_key])}")
|
|
1052
|
+
end
|
|
1053
|
+
true
|
|
1054
|
+
end
|
|
1055
|
+
|
|
1056
|
+
# Define method to get pending updates
|
|
1057
|
+
define_method("pending_#{resource.to_s.underscore}_updates".to_sym) do
|
|
1058
|
+
changed_keys = changed_attributes.keys.intersection(attributes.map(&:to_s))
|
|
1059
|
+
changed_keys.each_with_object({}) do |attr_name, payload|
|
|
1060
|
+
type = if self.class.respond_to?(:attribute_types)
|
|
1061
|
+
self.class.attribute_types[attr_name.to_s]
|
|
1062
|
+
end
|
|
1063
|
+
payload[attr_name] = ActiveRpc::ModelExtensions.prepare_value_for_rpc(send(attr_name), type)
|
|
1064
|
+
end
|
|
1065
|
+
end
|
|
1066
|
+
|
|
1067
|
+
# Define method to create a new resource and associate it with this model
|
|
1068
|
+
define_method("create_and_associate_#{resource.to_s.underscore}".to_sym) do |attributes = {}|
|
|
1069
|
+
# Prepare parameters for the create call
|
|
1070
|
+
params = {}
|
|
1071
|
+
normalized_attributes =
|
|
1072
|
+
if attributes.respond_to?(:to_h)
|
|
1073
|
+
attributes.to_h
|
|
1074
|
+
else
|
|
1075
|
+
attributes || {}
|
|
1076
|
+
end
|
|
1077
|
+
|
|
1078
|
+
normalized_attributes =
|
|
1079
|
+
if normalized_attributes.respond_to?(:with_indifferent_access)
|
|
1080
|
+
normalized_attributes.with_indifferent_access
|
|
1081
|
+
else
|
|
1082
|
+
normalized_attributes
|
|
1083
|
+
end
|
|
1084
|
+
|
|
1085
|
+
# Only include attributes that are defined in the proto
|
|
1086
|
+
self.class.active_rpc_config[:attributes].each do |attr|
|
|
1087
|
+
next unless normalized_attributes.respond_to?(:key?)
|
|
1088
|
+
next unless normalized_attributes.key?(attr) || normalized_attributes.key?(attr.to_s)
|
|
1089
|
+
|
|
1090
|
+
value = normalized_attributes[attr] || normalized_attributes[attr.to_s]
|
|
1091
|
+
type = if self.class.respond_to?(:attribute_types)
|
|
1092
|
+
self.class.attribute_types[attr.to_s]
|
|
1093
|
+
end
|
|
1094
|
+
params[attr] = ActiveRpc::ModelExtensions.prepare_value_for_rpc(value, type)
|
|
1095
|
+
end
|
|
1096
|
+
|
|
1097
|
+
# Include any additional attributes that might be needed for the RPC call
|
|
1098
|
+
# but are not part of the model's attributes (like password, avatar_image, etc.)
|
|
1099
|
+
normalized_attributes.each do |key, value|
|
|
1100
|
+
key_sym = key.to_sym
|
|
1101
|
+
next if params.key?(key_sym)
|
|
1102
|
+
|
|
1103
|
+
type = if self.class.respond_to?(:attribute_types)
|
|
1104
|
+
self.class.attribute_types[key.to_s]
|
|
1105
|
+
end
|
|
1106
|
+
params[key_sym] = ActiveRpc::ModelExtensions.prepare_value_for_rpc(value, type)
|
|
1107
|
+
end
|
|
1108
|
+
|
|
1109
|
+
Rails.logger.info("[ActiveRpc] Creating new #{resource} with params: #{params.keys}")
|
|
1110
|
+
|
|
1111
|
+
# Call the CreateUser method on the gRPC service
|
|
1112
|
+
# Add field tracking metadata to help with protobuf field presence
|
|
1113
|
+
params_with_fields = params.to_h
|
|
1114
|
+
params_with_fields[:_fields] = ActiveRpc::ModelExtensions._active_rpc_extract_field_paths(params)
|
|
1115
|
+
|
|
1116
|
+
response = self.class.grpc_call_with_locale("Create#{resource}".to_sym, params_with_fields)
|
|
1117
|
+
|
|
1118
|
+
# Check if the creation was successful by looking at _metadata first
|
|
1119
|
+
if response&.message&.respond_to?(:_metadata) && response.message._metadata&.success == false
|
|
1120
|
+
# This is a structured error response, handle validation errors
|
|
1121
|
+
Rails.logger.debug('[ActiveRpc] Received structured error response')
|
|
1122
|
+
|
|
1123
|
+
# Process the structured validation errors
|
|
1124
|
+
validation_errors_added = false
|
|
1125
|
+
if response.message._metadata.errors && response.message._metadata.errors.length > 0
|
|
1126
|
+
response.message._metadata.errors.each do |field, error_messages|
|
|
1127
|
+
error_messages.messages.each { |msg| errors.add(field.to_sym, msg) }
|
|
1128
|
+
end
|
|
1129
|
+
validation_errors_added = true
|
|
1130
|
+
Rails.logger.debug('[ActiveRpc] Added structured validation errors')
|
|
1131
|
+
end
|
|
1132
|
+
|
|
1133
|
+
unless validation_errors_added
|
|
1134
|
+
errors.add(options[:foreign_key],
|
|
1135
|
+
"An error occurred while creating the #{resource.to_s.underscore} record. Please try again.")
|
|
1136
|
+
end
|
|
1137
|
+
|
|
1138
|
+
Rails.logger.debug('[ActiveRpc] create_and_associate result: false, foreign_key: nil')
|
|
1139
|
+
false
|
|
1140
|
+
elsif response&.message&.respond_to?(:id)
|
|
1141
|
+
new_id = response.message.id
|
|
1142
|
+
Rails.logger.info("[ActiveRpc] Successfully created #{resource} with ID #{new_id}")
|
|
1143
|
+
|
|
1144
|
+
# Set the foreign key
|
|
1145
|
+
send("#{options[:foreign_key]}=", new_id)
|
|
1146
|
+
|
|
1147
|
+
# Update attributes with the response data
|
|
1148
|
+
if response.message.respond_to?(:to_h)
|
|
1149
|
+
# Convert the response message to a hash
|
|
1150
|
+
response_data = response.message.to_h
|
|
1151
|
+
|
|
1152
|
+
# Use intersection to get only the attributes that exist in both the response and our model
|
|
1153
|
+
attrs_to_assign = response_data.slice(*self.class.active_rpc_config[:attributes].map(&:to_sym))
|
|
1154
|
+
|
|
1155
|
+
# Assign the attributes in one go
|
|
1156
|
+
assign_attributes(attrs_to_assign)
|
|
1157
|
+
end
|
|
1158
|
+
|
|
1159
|
+
# Clear remote attribute changes since these are now the "original" values
|
|
1160
|
+
clear_attribute_changes(rpc_attribute_names) if respond_to?(:clear_attribute_changes)
|
|
1161
|
+
|
|
1162
|
+
Rails.logger.debug("[ActiveRpc] create_and_associate result: true, foreign_key: #{new_id}")
|
|
1163
|
+
true
|
|
1164
|
+
else
|
|
1165
|
+
# Check for validation errors
|
|
1166
|
+
if response&.error
|
|
1167
|
+
error_code = response.error.respond_to?(:code) ? response.error.code : 'UNKNOWN'
|
|
1168
|
+
error_details = response.error.respond_to?(:message) ? response.error.message : 'Unknown error'
|
|
1169
|
+
error_message = "Server returned error: #{error_code} - #{error_details}"
|
|
1170
|
+
|
|
1171
|
+
# Handle validation errors - check for shared error object first
|
|
1172
|
+
validation_errors_added = false
|
|
1173
|
+
|
|
1174
|
+
# Check for shared error object in response metadata (legacy fallback)
|
|
1175
|
+
if response.respond_to?(:_metadata) && response._metadata && !response._metadata.success && response._metadata.errors && response._metadata.errors.length > 0
|
|
1176
|
+
response._metadata.errors.each do |field, error_messages|
|
|
1177
|
+
error_messages.messages.each { |msg| errors.add(field.to_sym, msg) }
|
|
1178
|
+
end
|
|
1179
|
+
validation_errors_added = true
|
|
1180
|
+
end
|
|
1181
|
+
|
|
1182
|
+
# If no structured errors found, add generic error
|
|
1183
|
+
unless validation_errors_added
|
|
1184
|
+
errors.add(options[:foreign_key],
|
|
1185
|
+
"An error occurred while creating the #{resource.to_s.underscore} record. Please try again.")
|
|
1186
|
+
end
|
|
1187
|
+
|
|
1188
|
+
# If no validation errors were added, add a generic error
|
|
1189
|
+
unless validation_errors_added
|
|
1190
|
+
# Check if we have a meaningful error message to show
|
|
1191
|
+
if error_details.present? && error_details != 'Unknown error'
|
|
1192
|
+
# Clean up the error message - remove gRPC debug info
|
|
1193
|
+
clean_error = error_details.gsub(/\. debug_error_string:.+$/, '')
|
|
1194
|
+
errors.add(options[:foreign_key], clean_error)
|
|
1195
|
+
else
|
|
1196
|
+
errors.add(options[:foreign_key],
|
|
1197
|
+
"An error occurred while creating the #{resource.to_s.underscore} record. Please try again.")
|
|
1198
|
+
end
|
|
1199
|
+
end
|
|
1200
|
+
|
|
1201
|
+
Rails.logger.error("[ActiveRpc] Failed to create #{resource}: #{error_message}")
|
|
1202
|
+
else
|
|
1203
|
+
Rails.logger.error("[ActiveRpc] Failed to create #{resource}: No valid response")
|
|
1204
|
+
errors.add(options[:foreign_key],
|
|
1205
|
+
"An error occurred while creating the #{resource.to_s.underscore} record. Please try again.")
|
|
1206
|
+
end
|
|
1207
|
+
|
|
1208
|
+
Rails.logger.debug('[ActiveRpc] create_and_associate result: false, foreign_key: nil')
|
|
1209
|
+
false
|
|
1210
|
+
end
|
|
1211
|
+
rescue StandardError => e
|
|
1212
|
+
# Handle any exceptions that might occur during the RPC call
|
|
1213
|
+
error_message = e.message
|
|
1214
|
+
|
|
1215
|
+
# Check if this is a connection error
|
|
1216
|
+
if error_message.include?('Connection refused') ||
|
|
1217
|
+
error_message.include?('failed to connect') ||
|
|
1218
|
+
error_message.include?('Deadline exceeded') ||
|
|
1219
|
+
error_message.include?('UNAVAILABLE')
|
|
1220
|
+
# This is a connection error, so the service is likely down
|
|
1221
|
+
Rails.logger.error("[ActiveRpc] Failed to connect to #{resource} service: #{error_message}")
|
|
1222
|
+
errors.add(options[:foreign_key], 'Unable to connect to the required service. Please try again later.')
|
|
1223
|
+
else
|
|
1224
|
+
# Add generic error for any other exceptions
|
|
1225
|
+
Rails.logger.error("[ActiveRpc] Error creating #{resource}: #{error_message}")
|
|
1226
|
+
errors.add(options[:foreign_key],
|
|
1227
|
+
"An error occurred while creating the #{resource.to_s.underscore} record. Please try again.")
|
|
1228
|
+
end
|
|
1229
|
+
|
|
1230
|
+
# Return false to indicate failure
|
|
1231
|
+
false
|
|
1232
|
+
end
|
|
1233
|
+
|
|
1234
|
+
# Define method to get query configuration
|
|
1235
|
+
define_singleton_method("#{resource.to_s.underscore}_query_config") do
|
|
1236
|
+
active_rpc_config[:query_config] || {}
|
|
1237
|
+
end
|
|
1238
|
+
|
|
1239
|
+
# Define method to get available scopes
|
|
1240
|
+
define_singleton_method("#{resource.to_s.underscore}_scopes") do
|
|
1241
|
+
active_rpc_config[:scopes] || {}
|
|
1242
|
+
end
|
|
1243
|
+
|
|
1244
|
+
# Define class method for searching with query parameters
|
|
1245
|
+
define_singleton_method("search_#{resource.to_s.pluralize.underscore}") do |query_options = {}|
|
|
1246
|
+
# Prepare parameters for the search call
|
|
1247
|
+
params = {}
|
|
1248
|
+
|
|
1249
|
+
# Add search parameters if provided
|
|
1250
|
+
params[:q] = query_options[:q] if query_options[:q].present?
|
|
1251
|
+
|
|
1252
|
+
# Add scope parameters if provided
|
|
1253
|
+
params[:scope] = query_options[:scope] if query_options[:scope].present?
|
|
1254
|
+
|
|
1255
|
+
# Add include parameters if provided
|
|
1256
|
+
params[:include] = query_options[:include] if query_options[:include].present?
|
|
1257
|
+
|
|
1258
|
+
# Add pagination parameters ONLY if explicitly provided
|
|
1259
|
+
if query_options[:page].present? && query_options[:per_page].present?
|
|
1260
|
+
params[:page] = query_options[:page]
|
|
1261
|
+
params[:per_page] = query_options[:per_page]
|
|
1262
|
+
end
|
|
1263
|
+
|
|
1264
|
+
# Add sort parameters if provided
|
|
1265
|
+
params[:sort] = query_options[:sort] if query_options[:sort].present?
|
|
1266
|
+
|
|
1267
|
+
# Call the List method on the gRPC service
|
|
1268
|
+
# Add field tracking metadata to help with protobuf field presence
|
|
1269
|
+
params_with_fields = params.to_h
|
|
1270
|
+
params_with_fields[:_fields] = ActiveRpc::ModelExtensions._active_rpc_extract_field_paths(params)
|
|
1271
|
+
|
|
1272
|
+
response = grpc_call_with_locale("List#{resource}s", params_with_fields)
|
|
1273
|
+
|
|
1274
|
+
# Return the items from the response
|
|
1275
|
+
response&.message&.respond_to?(:items) ? response.message.items : []
|
|
1276
|
+
rescue StandardError => e
|
|
1277
|
+
Rails.logger.error("Failed to search #{resource} data: #{e.message}")
|
|
1278
|
+
[]
|
|
1279
|
+
end
|
|
1280
|
+
|
|
1281
|
+
# Update the bulk method to support query parameters
|
|
1282
|
+
define_singleton_method(options[:bulk_method]) do |ids = [], query_options = {}|
|
|
1283
|
+
if defined?(Rails)
|
|
1284
|
+
Rails.logger.debug("[ActiveRpc] #{options[:bulk_method]} called with IDs: #{ids.size} items, query_options: #{query_options.keys}")
|
|
1285
|
+
end
|
|
1286
|
+
|
|
1287
|
+
# Don't return early if we have query options (scope, where, etc.)
|
|
1288
|
+
# This allows filtering without specific IDs
|
|
1289
|
+
has_query_conditions = query_options.present? && (
|
|
1290
|
+
query_options[:scope].present? ||
|
|
1291
|
+
query_options[:where].present? ||
|
|
1292
|
+
query_options[:q].present?
|
|
1293
|
+
)
|
|
1294
|
+
|
|
1295
|
+
return [] if ids.empty? && !has_query_conditions
|
|
1296
|
+
|
|
1297
|
+
begin
|
|
1298
|
+
# Prepare parameters for the bulk call
|
|
1299
|
+
# Format IDs based on the foreign key column type
|
|
1300
|
+
formatted_ids = ids.map do |id|
|
|
1301
|
+
ActiveRpc::ModelExtensions.format_foreign_key_value(self, options[:foreign_key], id)
|
|
1302
|
+
end
|
|
1303
|
+
params = { ids: formatted_ids }
|
|
1304
|
+
Rails.logger.debug("[ActiveRpc] gRPC params prepared with #{formatted_ids.size} IDs") if defined?(Rails)
|
|
1305
|
+
|
|
1306
|
+
# Add search parameters if provided
|
|
1307
|
+
params[:q] = query_options[:q] if query_options[:q].present?
|
|
1308
|
+
|
|
1309
|
+
# Add ActiveRecord-style where filters if provided
|
|
1310
|
+
params[:where] = query_options[:where] if query_options[:where].present?
|
|
1311
|
+
|
|
1312
|
+
# Add scope parameters if provided
|
|
1313
|
+
if query_options[:scope].present?
|
|
1314
|
+
# Serialize entire scope arguments as single JSON payload
|
|
1315
|
+
# This ensures consistent handling of all argument types (arrays, hashes, objects, etc.)
|
|
1316
|
+
params[:scope] = { 'args' => query_options[:scope].to_json }
|
|
1317
|
+
if defined?(Rails)
|
|
1318
|
+
Rails.logger.debug("[ActiveRpc] Serialized scope args as JSON: #{query_options[:scope].keys}")
|
|
1319
|
+
end
|
|
1320
|
+
end
|
|
1321
|
+
|
|
1322
|
+
# Add include parameters if provided
|
|
1323
|
+
params[:include] = query_options[:include] if query_options[:include].present?
|
|
1324
|
+
|
|
1325
|
+
# Add pagination parameters
|
|
1326
|
+
if query_options[:page].present? && query_options[:per_page].present?
|
|
1327
|
+
params[:page] = query_options[:page]
|
|
1328
|
+
params[:per_page] = query_options[:per_page]
|
|
1329
|
+
if defined?(Rails)
|
|
1330
|
+
Rails.logger.debug("[ActiveRpc] Using provided pagination: page=#{params[:page]}, per_page=#{params[:per_page]}")
|
|
1331
|
+
end
|
|
1332
|
+
elsif defined?(Rails)
|
|
1333
|
+
# For bulk operations, don't set per_page to get ALL results
|
|
1334
|
+
if defined?(Rails)
|
|
1335
|
+
Rails.logger.debug('[ActiveRpc] Bulk operation without pagination limit - fetching all results')
|
|
1336
|
+
end
|
|
1337
|
+
end
|
|
1338
|
+
|
|
1339
|
+
# Add sort parameters if provided
|
|
1340
|
+
params[:sort] = query_options[:sort] if query_options[:sort].present?
|
|
1341
|
+
|
|
1342
|
+
# Call the List method on the gRPC service
|
|
1343
|
+
# Add field tracking metadata to help with protobuf field presence
|
|
1344
|
+
params_with_fields = params.to_h
|
|
1345
|
+
params_with_fields[:_fields] = ActiveRpc::ModelExtensions._active_rpc_extract_field_paths(params)
|
|
1346
|
+
|
|
1347
|
+
Rails.logger.debug("[ActiveRpc] Making gRPC call: List#{resource}s") if defined?(Rails)
|
|
1348
|
+
|
|
1349
|
+
begin
|
|
1350
|
+
response = grpc_call_with_locale("List#{resource}s", params_with_fields)
|
|
1351
|
+
|
|
1352
|
+
if response.nil?
|
|
1353
|
+
Rails.logger.error('[ActiveRpc] gRPC response is nil') if defined?(Rails)
|
|
1354
|
+
else
|
|
1355
|
+
item_count = if response&.message.respond_to?(:users)
|
|
1356
|
+
response.message.users.size
|
|
1357
|
+
elsif response&.message.respond_to?(:items)
|
|
1358
|
+
response.message.items.size
|
|
1359
|
+
else
|
|
1360
|
+
0
|
|
1361
|
+
end
|
|
1362
|
+
if defined?(Rails)
|
|
1363
|
+
Rails.logger.debug("[ActiveRpc] gRPC call completed successfully, received #{item_count} items")
|
|
1364
|
+
end
|
|
1365
|
+
end
|
|
1366
|
+
rescue StandardError => e
|
|
1367
|
+
Rails.logger.error("[ActiveRpc] gRPC call failed: #{e.class}: #{e.message}") if defined?(Rails)
|
|
1368
|
+
raise e
|
|
1369
|
+
end
|
|
1370
|
+
|
|
1371
|
+
# Return the items from the response
|
|
1372
|
+
result = if response&.message&.respond_to?(:users)
|
|
1373
|
+
response.message.users
|
|
1374
|
+
elsif response&.message&.respond_to?(:items)
|
|
1375
|
+
response.message.items
|
|
1376
|
+
else
|
|
1377
|
+
Rails.logger.warn('[ActiveRpc] No users or items found in gRPC response') if defined?(Rails)
|
|
1378
|
+
[]
|
|
1379
|
+
end
|
|
1380
|
+
|
|
1381
|
+
Rails.logger.debug("[ActiveRpc] Returning #{result.size} items from gRPC call") if defined?(Rails)
|
|
1382
|
+
result
|
|
1383
|
+
rescue StandardError => e
|
|
1384
|
+
Rails.logger.error("Failed to fetch bulk #{resource} data for ids=#{ids}: #{e.message}")
|
|
1385
|
+
[]
|
|
1386
|
+
end
|
|
1387
|
+
end
|
|
1388
|
+
end
|
|
1389
|
+
end
|
|
1390
|
+
|
|
1391
|
+
# Module-level method to extract all field paths from a nested hash structure
|
|
1392
|
+
# Examples:
|
|
1393
|
+
# {id: "22", user_roles_list: []} -> ["id", "user_roles_list"]
|
|
1394
|
+
# {id: "22", avatar_attributes: {filename: "test.jpg"}} -> ["id", "avatar_attributes", "avatar_attributes.filename"]
|
|
1395
|
+
# {user_roles_list: [{id: "1", role_id: "admin"}]} -> ["user_roles_list", "user_roles_list[0]", "user_roles_list[0].id", "user_roles_list[0].role_id"]
|
|
1396
|
+
def self._active_rpc_extract_field_paths(params, prefix = '')
|
|
1397
|
+
return [] unless params.respond_to?(:each)
|
|
1398
|
+
|
|
1399
|
+
field_paths = []
|
|
1400
|
+
|
|
1401
|
+
params.each do |key, value|
|
|
1402
|
+
# Skip internal fields (starting with _)
|
|
1403
|
+
next if key.to_s.start_with?('_')
|
|
1404
|
+
|
|
1405
|
+
current_path = prefix.empty? ? key.to_s : "#{prefix}.#{key}"
|
|
1406
|
+
|
|
1407
|
+
# Always include the current field path
|
|
1408
|
+
field_paths << current_path
|
|
1409
|
+
|
|
1410
|
+
if value.is_a?(Hash) && value.any?
|
|
1411
|
+
# Preserve original dot notation for nested hashes
|
|
1412
|
+
field_paths.concat(_active_rpc_extract_field_paths(value, current_path))
|
|
1413
|
+
|
|
1414
|
+
# Also emit bracket notation so protobuf map fields keep key-level metadata
|
|
1415
|
+
value.each do |map_key, map_value|
|
|
1416
|
+
map_path = "#{current_path}[#{map_key}]"
|
|
1417
|
+
field_paths << map_path
|
|
1418
|
+
field_paths.concat(_active_rpc_extract_field_paths(map_value, map_path))
|
|
1419
|
+
end
|
|
1420
|
+
next
|
|
1421
|
+
end
|
|
1422
|
+
|
|
1423
|
+
# Handle arrays with hash elements (like user_roles_list)
|
|
1424
|
+
next unless value.is_a?(Array)
|
|
1425
|
+
|
|
1426
|
+
# Include the array field itself even if empty
|
|
1427
|
+
value.each_with_index do |item, index|
|
|
1428
|
+
next unless item.is_a?(Hash)
|
|
1429
|
+
|
|
1430
|
+
item_path = "#{current_path}[#{index}]"
|
|
1431
|
+
field_paths << item_path
|
|
1432
|
+
field_paths.concat(_active_rpc_extract_field_paths(item, item_path))
|
|
1433
|
+
end
|
|
1434
|
+
end
|
|
1435
|
+
|
|
1436
|
+
field_paths.uniq
|
|
1437
|
+
end
|
|
1438
|
+
|
|
1439
|
+
# Helper method to format foreign key values based on database column type
|
|
1440
|
+
# This ensures compatibility between ActiveRecord column types and protobuf field types
|
|
1441
|
+
def self.format_foreign_key_value(model_class, foreign_key, value)
|
|
1442
|
+
return nil if value.blank?
|
|
1443
|
+
|
|
1444
|
+
# Get the column type from the model's database schema
|
|
1445
|
+
column = model_class.columns_hash[foreign_key.to_s]
|
|
1446
|
+
|
|
1447
|
+
if %i[integer bigint].include?(column&.type)
|
|
1448
|
+
# Send as integer for integer/bigint columns to match protobuf int64 fields
|
|
1449
|
+
value.to_i
|
|
1450
|
+
else
|
|
1451
|
+
# Send as string for all other types (string, uuid, etc.)
|
|
1452
|
+
value.to_s
|
|
1453
|
+
end
|
|
1454
|
+
end
|
|
1455
|
+
end
|
|
1456
|
+
end
|