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,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveRpc
|
|
4
|
+
module Rpc
|
|
5
|
+
# Base controller class that includes all our concerns
|
|
6
|
+
class BaseController < ::Gruf::Controllers::Base
|
|
7
|
+
include ActiveRpc::Rpc::Concerns::RequestProcessor
|
|
8
|
+
include ActiveRpc::Rpc::Concerns::QueryBuilder
|
|
9
|
+
include ActiveRpc::Rpc::Concerns::ResourceController
|
|
10
|
+
include ActiveRpc::Rpc::Concerns::Serializable
|
|
11
|
+
|
|
12
|
+
# Add common methods for all gRPC controllers here
|
|
13
|
+
|
|
14
|
+
private
|
|
15
|
+
|
|
16
|
+
# Provide a params method that Pagy can use
|
|
17
|
+
# Converts the protobuf request into a Rails-style params hash
|
|
18
|
+
def params
|
|
19
|
+
@params ||= build_params_from_request
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def build_params_from_request
|
|
23
|
+
return {} unless request&.message
|
|
24
|
+
|
|
25
|
+
request_message = request.message
|
|
26
|
+
params_hash = {}
|
|
27
|
+
|
|
28
|
+
# Extract all supported parameters
|
|
29
|
+
extract_pagination_params(request_message, params_hash)
|
|
30
|
+
extract_query_params(request_message, params_hash)
|
|
31
|
+
extract_other_params(request_message, params_hash)
|
|
32
|
+
|
|
33
|
+
params_hash
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def extract_pagination_params(request_message, params_hash)
|
|
37
|
+
params_hash[:page] = request_message.page if request_message.respond_to?(:page)
|
|
38
|
+
params_hash[:per_page] = request_message.per_page if request_message.respond_to?(:per_page)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def extract_query_params(request_message, params_hash)
|
|
42
|
+
if request_message.respond_to?(:q) && request_message.q
|
|
43
|
+
params_hash[:q] = convert_protobuf_value(request_message.q)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
return unless request_message.respond_to?(:scope) && request_message.scope
|
|
47
|
+
|
|
48
|
+
params_hash[:scope] = convert_protobuf_value(request_message.scope)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def extract_other_params(request_message, params_hash)
|
|
52
|
+
params_hash[:sort] = request_message.sort if request_message.respond_to?(:sort)
|
|
53
|
+
params_hash[:include] = request_message.include if request_message.respond_to?(:include)
|
|
54
|
+
params_hash[:ids] = request_message.ids if request_message.respond_to?(:ids) && request_message.ids
|
|
55
|
+
params_hash[:where] = request_message.where.to_h if request_message.respond_to?(:where) && request_message.where
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def convert_protobuf_value(value)
|
|
59
|
+
value.respond_to?(:to_h) ? value.to_h : value
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
module ActiveRpc
|
|
2
|
+
module Rpc
|
|
3
|
+
module Concerns
|
|
4
|
+
# The Includable concern provides methods for applying includes (eager loading)
|
|
5
|
+
# to ActiveRecord queries in gRPC controllers.
|
|
6
|
+
#
|
|
7
|
+
# @example
|
|
8
|
+
# def list_users
|
|
9
|
+
# process_request do
|
|
10
|
+
# base_query = User.all
|
|
11
|
+
# query = apply_includes(base_query, request.message)
|
|
12
|
+
# # ...
|
|
13
|
+
# end
|
|
14
|
+
# end
|
|
15
|
+
module Includable
|
|
16
|
+
extend ActiveSupport::Concern
|
|
17
|
+
|
|
18
|
+
# Apply includes to a query
|
|
19
|
+
def apply_includes(query, params)
|
|
20
|
+
return query unless params.respond_to?(:include) && params.include.present?
|
|
21
|
+
|
|
22
|
+
begin
|
|
23
|
+
# Convert the array to symbols
|
|
24
|
+
includes = params.include.map(&:to_sym)
|
|
25
|
+
|
|
26
|
+
# Get query configuration
|
|
27
|
+
query_config = get_query_config(query.model)
|
|
28
|
+
includable_assocs = query_config[:includable] || []
|
|
29
|
+
|
|
30
|
+
# Filter out non-includable associations if we have defined includable associations
|
|
31
|
+
filtered_includes = if includable_assocs.empty?
|
|
32
|
+
includes
|
|
33
|
+
else
|
|
34
|
+
includes.select do |assoc|
|
|
35
|
+
includable_assocs.include?(assoc)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Apply includes
|
|
40
|
+
query.includes(filtered_includes)
|
|
41
|
+
rescue StandardError => e
|
|
42
|
+
Rails.logger.error("Error applying includes: #{e.message}")
|
|
43
|
+
# Return original query if includes fails
|
|
44
|
+
query
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Get query configuration for a model
|
|
49
|
+
def get_query_config(model_class)
|
|
50
|
+
if model_class.respond_to?(:active_rpc_config) && model_class.active_rpc_config
|
|
51
|
+
resource = model_class.active_rpc_config[:resource].to_s.underscore
|
|
52
|
+
query_config_method = "#{resource}_query_config"
|
|
53
|
+
|
|
54
|
+
return model_class.send(query_config_method) if model_class.respond_to?(query_config_method)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Default configuration if none is defined
|
|
58
|
+
{
|
|
59
|
+
searchable: model_class.column_names,
|
|
60
|
+
filterable: model_class.column_names,
|
|
61
|
+
sortable: model_class.column_names,
|
|
62
|
+
includable: model_class.reflect_on_all_associations.map(&:name)
|
|
63
|
+
}
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
require 'pagy'
|
|
2
|
+
require 'pagy/classes/request'
|
|
3
|
+
require 'pagy/toolbox/paginators/offset'
|
|
4
|
+
|
|
5
|
+
module ActiveRpc
|
|
6
|
+
module Rpc
|
|
7
|
+
module Concerns
|
|
8
|
+
# The Paginatable concern provides methods for applying pagination
|
|
9
|
+
# to ActiveRecord queries in gRPC controllers.
|
|
10
|
+
#
|
|
11
|
+
# It supports:
|
|
12
|
+
# - Standard pagination with page and per_page parameters
|
|
13
|
+
# - Integration with the Pagy gem if available
|
|
14
|
+
# - Pagination metadata for responses
|
|
15
|
+
#
|
|
16
|
+
# @example
|
|
17
|
+
# def list_users
|
|
18
|
+
# process_request do
|
|
19
|
+
# base_query = User.all
|
|
20
|
+
# query = apply_pagination(base_query, request.message)
|
|
21
|
+
# # ...
|
|
22
|
+
# end
|
|
23
|
+
# end
|
|
24
|
+
module Paginatable
|
|
25
|
+
extend ActiveSupport::Concern
|
|
26
|
+
|
|
27
|
+
# Apply pagination to a query only if explicitly requested
|
|
28
|
+
def apply_pagination(query, params)
|
|
29
|
+
return query unless pagination_params?(params)
|
|
30
|
+
|
|
31
|
+
begin
|
|
32
|
+
page, per_page = sanitized_page_params(params)
|
|
33
|
+
base_options = defined?(Pagy::OPTIONS) ? Pagy::OPTIONS : Pagy::DEFAULT
|
|
34
|
+
options = base_options.merge(rpc_pagy_options(page, per_page))
|
|
35
|
+
pagy, records = Pagy::OffsetPaginator.paginate(query, options)
|
|
36
|
+
@last_pagy = pagy
|
|
37
|
+
records
|
|
38
|
+
rescue StandardError => e
|
|
39
|
+
Rails.logger.error("Error applying pagination: #{e.message}")
|
|
40
|
+
# Return original query if pagination fails
|
|
41
|
+
query
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Get pagination metadata
|
|
46
|
+
def pagination_metadata(query, params, total_count = nil)
|
|
47
|
+
# If Pagy was used, get metadata from it
|
|
48
|
+
if defined?(@last_pagy) && @last_pagy
|
|
49
|
+
return {
|
|
50
|
+
total_count: @last_pagy.count,
|
|
51
|
+
page: @last_pagy.page,
|
|
52
|
+
per_page: @last_pagy.limit,
|
|
53
|
+
total_pages: @last_pagy.last
|
|
54
|
+
}
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# If pagination is not requested, return metadata with all records
|
|
58
|
+
unless params.respond_to?(:page) && params.respond_to?(:per_page) &&
|
|
59
|
+
params.page.present? && params.per_page.present?
|
|
60
|
+
total_count = query.count
|
|
61
|
+
return {
|
|
62
|
+
total_count: total_count,
|
|
63
|
+
page: 1,
|
|
64
|
+
per_page: total_count,
|
|
65
|
+
total_pages: 1
|
|
66
|
+
}
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Calculate total count and pages
|
|
70
|
+
total_count ||= query.except(:limit, :offset).count
|
|
71
|
+
|
|
72
|
+
# Validate and sanitize per_page parameter
|
|
73
|
+
per_page = params.per_page.to_i
|
|
74
|
+
if per_page <= 0
|
|
75
|
+
Rails.logger.warn("Invalid per_page value: #{params.per_page}. Using default value.")
|
|
76
|
+
per_page = ActiveRpc::Rpc.configuration.default_per_page || 20
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Ensure per_page doesn't exceed maximum allowed
|
|
80
|
+
max_per_page = ActiveRpc::Rpc.configuration.max_per_page || 100
|
|
81
|
+
per_page = [ per_page, max_per_page ].min
|
|
82
|
+
|
|
83
|
+
# Calculate total pages safely
|
|
84
|
+
total_pages = total_count > 0 ? (total_count.to_f / per_page).ceil : 1
|
|
85
|
+
|
|
86
|
+
{
|
|
87
|
+
total_count: total_count,
|
|
88
|
+
page: params.page,
|
|
89
|
+
per_page: per_page,
|
|
90
|
+
total_pages: total_pages
|
|
91
|
+
}
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
private
|
|
95
|
+
|
|
96
|
+
def pagination_params?(params)
|
|
97
|
+
params.respond_to?(:page) && params.respond_to?(:per_page) &&
|
|
98
|
+
params.page.present? && params.per_page.present?
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def sanitized_page_params(params)
|
|
102
|
+
page = [ params.page.to_i, 1 ].max
|
|
103
|
+
per_page = [ [ params.per_page.to_i, 1 ].max, ActiveRpc::Rpc.configuration.max_per_page ].min
|
|
104
|
+
[page, per_page]
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def rpc_pagy_options(page, per_page)
|
|
108
|
+
page_key = (Pagy::OPTIONS[:page_key] || Pagy::DEFAULT[:page_key]).to_s
|
|
109
|
+
limit_key = (Pagy::OPTIONS[:limit_key] || Pagy::DEFAULT[:limit_key]).to_s
|
|
110
|
+
client_max = ActiveRpc::Rpc.configuration.max_per_page || Pagy::OPTIONS[:client_max_limit] || Pagy::DEFAULT[:limit]
|
|
111
|
+
|
|
112
|
+
request_payload = {
|
|
113
|
+
base_url: '',
|
|
114
|
+
path: '',
|
|
115
|
+
params: {
|
|
116
|
+
'page' => {
|
|
117
|
+
page_key => page,
|
|
118
|
+
limit_key => per_page
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
cookie: nil
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
request_wrapper = if Pagy::Request.instance_method(:initialize).parameters.any? { |type, _| type == :key || type == :keyreq }
|
|
125
|
+
Pagy::Request.new(
|
|
126
|
+
request: request_payload,
|
|
127
|
+
root_key: 'page',
|
|
128
|
+
page_key: page_key,
|
|
129
|
+
limit_key: limit_key,
|
|
130
|
+
client_max_limit: client_max,
|
|
131
|
+
jsonapi: true
|
|
132
|
+
)
|
|
133
|
+
else
|
|
134
|
+
Pagy::Request.new(
|
|
135
|
+
request_payload.transform_keys { |key| key == :params ? :query : key },
|
|
136
|
+
jsonapi: true,
|
|
137
|
+
page_key: page_key,
|
|
138
|
+
limit_key: limit_key,
|
|
139
|
+
client_max_limit: client_max
|
|
140
|
+
)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
{
|
|
144
|
+
request: request_wrapper,
|
|
145
|
+
jsonapi: true,
|
|
146
|
+
client_max_limit: client_max,
|
|
147
|
+
page_key: page_key,
|
|
148
|
+
limit_key: limit_key,
|
|
149
|
+
limit: per_page
|
|
150
|
+
}
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
module ActiveRpc
|
|
2
|
+
module Rpc
|
|
3
|
+
module Concerns
|
|
4
|
+
# The QueryBuilder concern combines all query-related concerns and provides
|
|
5
|
+
# high-level methods for building and executing queries in gRPC controllers.
|
|
6
|
+
#
|
|
7
|
+
# @example
|
|
8
|
+
# def list_users
|
|
9
|
+
# process_request do
|
|
10
|
+
# execute_query(User, request.message,
|
|
11
|
+
# response_class: ::Core::UserListResponse,
|
|
12
|
+
# transformer: :to_rpc_response
|
|
13
|
+
# )
|
|
14
|
+
# end
|
|
15
|
+
# end
|
|
16
|
+
module QueryBuilder
|
|
17
|
+
extend ActiveSupport::Concern
|
|
18
|
+
|
|
19
|
+
include ActiveRpc::Rpc::Concerns::Ransackable
|
|
20
|
+
include ActiveRpc::Rpc::Concerns::Scopable
|
|
21
|
+
include ActiveRpc::Rpc::Concerns::Includable
|
|
22
|
+
include ActiveRpc::Rpc::Concerns::Paginatable
|
|
23
|
+
include ActiveRpc::Rpc::Concerns::Sortable
|
|
24
|
+
|
|
25
|
+
# Build a query with all parameters
|
|
26
|
+
def build_query(base_query, params)
|
|
27
|
+
query = base_query
|
|
28
|
+
|
|
29
|
+
# Apply filters in a specific order
|
|
30
|
+
query = apply_where_filters(query, params)
|
|
31
|
+
query = apply_ransack(query, params)
|
|
32
|
+
query = apply_scopes(query, params)
|
|
33
|
+
query = apply_includes(query, params)
|
|
34
|
+
apply_sorting(query, params)
|
|
35
|
+
|
|
36
|
+
# TEMPORARILY DISABLE PAGINATION TO TEST BULK LOADING
|
|
37
|
+
# Apply pagination last
|
|
38
|
+
# query = apply_pagination(query, params)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Execute a query and return results with pagination metadata
|
|
42
|
+
def execute_query(query_or_class, request_payload, options = {})
|
|
43
|
+
# Determine the base query
|
|
44
|
+
base_query = if query_or_class.is_a?(Class)
|
|
45
|
+
# If a class is provided, start with all records
|
|
46
|
+
options[:base_query] || query_or_class.all
|
|
47
|
+
else
|
|
48
|
+
# If a query is provided, use it directly
|
|
49
|
+
query_or_class
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Filter by specific IDs if provided
|
|
53
|
+
if request_payload.respond_to?(:ids) && request_payload.ids.present?
|
|
54
|
+
Rails.logger.info("[DEBUG] IDs found: #{request_payload.ids.inspect} (#{request_payload.ids.class})")
|
|
55
|
+
|
|
56
|
+
# Convert Google::Protobuf::RepeatedField to Ruby Array
|
|
57
|
+
ids_array = if request_payload.ids.is_a?(Google::Protobuf::RepeatedField)
|
|
58
|
+
request_payload.ids.to_a
|
|
59
|
+
else
|
|
60
|
+
request_payload.ids
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
Rails.logger.info("[DEBUG] Converted IDs: #{ids_array.inspect} (#{ids_array.class})")
|
|
64
|
+
base_query = base_query.where(id: ids_array)
|
|
65
|
+
else
|
|
66
|
+
Rails.logger.info("[DEBUG] No IDs found. respond_to?(:ids): #{request_payload.respond_to?(:ids)}, ids.present?: #{request_payload.respond_to?(:ids) ? request_payload.ids.present? : 'N/A'}")
|
|
67
|
+
Rails.logger.info("[DEBUG] Request payload: #{request_payload.inspect}")
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Apply additional filters if provided
|
|
71
|
+
base_query = options[:additional_filters].call(base_query) if options[:additional_filters].is_a?(Proc)
|
|
72
|
+
|
|
73
|
+
# Apply includes from options if provided
|
|
74
|
+
# Note: includes are applied BEFORE scopes to avoid conflicts
|
|
75
|
+
base_query = base_query.includes(options[:includes]) if options[:includes].present?
|
|
76
|
+
|
|
77
|
+
# Build the query with all parameters
|
|
78
|
+
query = build_query(base_query, request_payload)
|
|
79
|
+
|
|
80
|
+
# Execute the query
|
|
81
|
+
records = query.to_a
|
|
82
|
+
|
|
83
|
+
# Transform records if a transformer is provided
|
|
84
|
+
if options[:transformer].is_a?(Proc)
|
|
85
|
+
records = records.map(&options[:transformer])
|
|
86
|
+
elsif options[:transformer] == :to_rpc_response
|
|
87
|
+
records = records.map(&:to_rpc_response)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Return the response with pagination metadata
|
|
91
|
+
response_class = options[:response_class]
|
|
92
|
+
if response_class
|
|
93
|
+
paginated_response(response_class, records, query, request_payload, records.size)
|
|
94
|
+
else
|
|
95
|
+
{
|
|
96
|
+
records: records,
|
|
97
|
+
metadata: pagination_metadata(query, request_payload, records.size)
|
|
98
|
+
}
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Get the response with pagination metadata
|
|
103
|
+
def paginated_response(response_class, items, query, params, total_count = nil)
|
|
104
|
+
# Get pagination metadata
|
|
105
|
+
metadata = pagination_metadata(query, params, total_count)
|
|
106
|
+
|
|
107
|
+
# Check if pagination was requested
|
|
108
|
+
pagination_requested = params.respond_to?(:page) && params.respond_to?(:per_page) &&
|
|
109
|
+
params.page.present? && params.per_page.present?
|
|
110
|
+
|
|
111
|
+
# Create the response with items and pagination metadata
|
|
112
|
+
response_class.new(
|
|
113
|
+
items: items,
|
|
114
|
+
total_count: metadata[:total_count],
|
|
115
|
+
page: pagination_requested ? metadata[:page] : 1,
|
|
116
|
+
per_page: pagination_requested ? metadata[:per_page] : metadata[:total_count],
|
|
117
|
+
total_pages: pagination_requested ? metadata[:total_pages] : 1
|
|
118
|
+
)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
private
|
|
122
|
+
|
|
123
|
+
# Apply ActiveRecord-style where conditions
|
|
124
|
+
def apply_where_filters(query, params)
|
|
125
|
+
return query unless params.respond_to?(:where) && params.where.present?
|
|
126
|
+
|
|
127
|
+
begin
|
|
128
|
+
# Convert protobuf map to hash if needed
|
|
129
|
+
where_conditions = params.where.respond_to?(:to_h) ? params.where.to_h : params.where
|
|
130
|
+
|
|
131
|
+
# Apply each condition
|
|
132
|
+
where_conditions.each do |field, value|
|
|
133
|
+
# Validate field is allowed (security)
|
|
134
|
+
if allowed_where_field?(query.model, field)
|
|
135
|
+
query = query.where(field => value)
|
|
136
|
+
else
|
|
137
|
+
Rails.logger.warn("Filtered out disallowed where field: #{field}")
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
query
|
|
142
|
+
rescue StandardError => e
|
|
143
|
+
Rails.logger.error("Error applying where filters: #{e.message}")
|
|
144
|
+
query
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Check if a field is allowed for where filtering
|
|
149
|
+
def allowed_where_field?(model_class, field)
|
|
150
|
+
# Get allowed fields from query config or default to column names
|
|
151
|
+
query_config = get_query_config(model_class)
|
|
152
|
+
allowed_fields = query_config[:filterable] || model_class.column_names
|
|
153
|
+
|
|
154
|
+
# Check if field is in allowed list
|
|
155
|
+
allowed_fields.include?(field.to_s) || allowed_fields.include?(field.to_sym)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Get query configuration for a model (from Ransackable concern)
|
|
159
|
+
def get_query_config(model_class)
|
|
160
|
+
if model_class.respond_to?(:active_rpc_config) && model_class.active_rpc_config
|
|
161
|
+
resource = model_class.active_rpc_config[:resource].to_s.underscore
|
|
162
|
+
query_config_method = "#{resource}_query_config"
|
|
163
|
+
|
|
164
|
+
return model_class.send(query_config_method) if model_class.respond_to?(query_config_method)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Default configuration if none is defined
|
|
168
|
+
{
|
|
169
|
+
searchable: model_class.column_names,
|
|
170
|
+
filterable: model_class.column_names,
|
|
171
|
+
sortable: model_class.column_names,
|
|
172
|
+
includable: model_class.reflect_on_all_associations.map(&:name)
|
|
173
|
+
}
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
end
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
module ActiveRpc
|
|
2
|
+
module Rpc
|
|
3
|
+
module Concerns
|
|
4
|
+
# The Ransackable concern provides methods for applying Ransack-based filtering
|
|
5
|
+
# to ActiveRecord queries in gRPC controllers.
|
|
6
|
+
#
|
|
7
|
+
# It supports:
|
|
8
|
+
# - Standard Ransack predicates (e.g., q[name_cont]=John)
|
|
9
|
+
# - Full-text search via q[search]=query
|
|
10
|
+
#
|
|
11
|
+
# @example
|
|
12
|
+
# def list_users
|
|
13
|
+
# process_request do
|
|
14
|
+
# base_query = User.all
|
|
15
|
+
# query = apply_ransack(base_query, request.message)
|
|
16
|
+
# # ...
|
|
17
|
+
# end
|
|
18
|
+
# end
|
|
19
|
+
module Ransackable
|
|
20
|
+
extend ActiveSupport::Concern
|
|
21
|
+
|
|
22
|
+
# Apply Ransack search parameters to a query
|
|
23
|
+
def apply_ransack(query, params)
|
|
24
|
+
return query unless params.respond_to?(:q) && params.q.present?
|
|
25
|
+
|
|
26
|
+
begin
|
|
27
|
+
# Convert the map to a hash
|
|
28
|
+
search_params = params.q.to_h
|
|
29
|
+
|
|
30
|
+
# Get query configuration
|
|
31
|
+
query_config = get_query_config(query.model)
|
|
32
|
+
searchable_attrs = query_config[:searchable] || []
|
|
33
|
+
|
|
34
|
+
# Filter out non-searchable attributes
|
|
35
|
+
filtered_params = {}
|
|
36
|
+
search_params.each do |key, value|
|
|
37
|
+
# Extract the attribute name from the Ransack predicate
|
|
38
|
+
attr_name = key.to_s.split('_').first
|
|
39
|
+
|
|
40
|
+
# Include the parameter if the attribute is searchable
|
|
41
|
+
if searchable_attrs.include?(attr_name.to_sym) || searchable_attrs.include?(attr_name.to_s) || searchable_attrs.empty?
|
|
42
|
+
filtered_params[key] = value
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Check for special search parameter
|
|
47
|
+
if ActiveRpc::Rpc.configuration.enable_full_text_search &&
|
|
48
|
+
filtered_params.key?('search') && filtered_params['search'].present?
|
|
49
|
+
search_query = filtered_params.delete('search')
|
|
50
|
+
query = apply_full_text_search(query, search_query)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Apply standard Ransack filters
|
|
54
|
+
query.ransack(filtered_params).result
|
|
55
|
+
rescue => e
|
|
56
|
+
Rails.logger.error("Error applying ransack: #{e.message}")
|
|
57
|
+
# Return original query if ransack fails
|
|
58
|
+
query
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
# Apply full-text search to a query
|
|
65
|
+
def apply_full_text_search(query, search_query)
|
|
66
|
+
resource_class = query.model
|
|
67
|
+
|
|
68
|
+
# Try different search method patterns in order of preference
|
|
69
|
+
resource_name = resource_class.name.underscore.pluralize
|
|
70
|
+
|
|
71
|
+
# 1. Try model-specific search method
|
|
72
|
+
specific_search_method = "search_#{resource_name}"
|
|
73
|
+
if resource_class.respond_to?(specific_search_method)
|
|
74
|
+
return resource_class.send(specific_search_method, search_query)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# 2. Try generic search method
|
|
78
|
+
if resource_class.respond_to?(:search)
|
|
79
|
+
return resource_class.search(search_query)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# 3. Try full_text_search method
|
|
83
|
+
if resource_class.respond_to?(:full_text_search)
|
|
84
|
+
return resource_class.full_text_search(search_query)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# If no search method is found, return the original query
|
|
88
|
+
query
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Get query configuration for a model
|
|
92
|
+
def get_query_config(model_class)
|
|
93
|
+
if model_class.respond_to?(:active_rpc_config) && model_class.active_rpc_config
|
|
94
|
+
resource = model_class.active_rpc_config[:resource].to_s.underscore
|
|
95
|
+
query_config_method = "#{resource}_query_config"
|
|
96
|
+
|
|
97
|
+
if model_class.respond_to?(query_config_method)
|
|
98
|
+
return model_class.send(query_config_method)
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Default configuration if none is defined
|
|
103
|
+
{
|
|
104
|
+
searchable: model_class.column_names,
|
|
105
|
+
filterable: model_class.column_names,
|
|
106
|
+
sortable: model_class.column_names,
|
|
107
|
+
includable: model_class.reflect_on_all_associations.map(&:name)
|
|
108
|
+
}
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|