rhino_project_core 0.20.0.alpha.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +28 -0
- data/Rakefile +35 -0
- data/app/assets/stripe_flow.png +0 -0
- data/app/controllers/concerns/rhino/authenticated.rb +18 -0
- data/app/controllers/concerns/rhino/error_handling.rb +60 -0
- data/app/controllers/concerns/rhino/paper_trail_whodunnit.rb +11 -0
- data/app/controllers/concerns/rhino/permit.rb +38 -0
- data/app/controllers/concerns/rhino/set_current_user.rb +13 -0
- data/app/controllers/rhino/account_controller.rb +34 -0
- data/app/controllers/rhino/active_model_extension_controller.rb +52 -0
- data/app/controllers/rhino/base_controller.rb +23 -0
- data/app/controllers/rhino/crud_controller.rb +57 -0
- data/app/controllers/rhino/simple_controller.rb +13 -0
- data/app/controllers/rhino/simple_stream_controller.rb +12 -0
- data/app/helpers/rhino/omniauth_helper.rb +67 -0
- data/app/helpers/rhino/policy_helper.rb +42 -0
- data/app/helpers/rhino/segment_helper.rb +62 -0
- data/app/models/rhino/account.rb +13 -0
- data/app/models/rhino/current.rb +7 -0
- data/app/models/rhino/user.rb +44 -0
- data/app/overrides/active_record/autosave_association_override.rb +18 -0
- data/app/overrides/active_record/delegated_type_override.rb +14 -0
- data/app/overrides/activestorage/direct_uploads_controller_override.rb +23 -0
- data/app/overrides/activestorage/redirect_controller_override.rb +21 -0
- data/app/overrides/activestorage/redirect_representation_controller_override.rb +21 -0
- data/app/overrides/devise_token_auth/confirmations_controller_override.rb +14 -0
- data/app/overrides/devise_token_auth/omniauth_callbacks_controller_override.rb +45 -0
- data/app/overrides/devise_token_auth/passwords_controller_override.rb +9 -0
- data/app/overrides/devise_token_auth/registrations_controller_override.rb +20 -0
- data/app/overrides/devise_token_auth/sessions_controller_override.rb +26 -0
- data/app/overrides/devise_token_auth/token_validations_controller_override.rb +18 -0
- data/app/policies/rhino/account_policy.rb +27 -0
- data/app/policies/rhino/active_storage_attachment_policy.rb +16 -0
- data/app/policies/rhino/admin_policy.rb +20 -0
- data/app/policies/rhino/base_policy.rb +72 -0
- data/app/policies/rhino/crud_policy.rb +109 -0
- data/app/policies/rhino/editor_policy.rb +12 -0
- data/app/policies/rhino/global_policy.rb +8 -0
- data/app/policies/rhino/resource_info_policy.rb +9 -0
- data/app/policies/rhino/user_policy.rb +20 -0
- data/app/policies/rhino/viewer_policy.rb +19 -0
- data/app/resources/rhino/info_graph.rb +41 -0
- data/app/resources/rhino/open_api_info.rb +108 -0
- data/config/routes.rb +19 -0
- data/db/migrate/20180101000000_devise_token_auth_create_users.rb +54 -0
- data/db/migrate/20180622142754_add_allow_change_password_to_users.rb +5 -0
- data/db/migrate/20191217010224_add_approved_to_users.rb +7 -0
- data/db/migrate/20200503182019_change_tokens_to_json_b.rb +9 -0
- data/lib/commands/rhino/module/coverage_command.rb +44 -0
- data/lib/commands/rhino/module/dummy_command.rb +43 -0
- data/lib/commands/rhino/module/new_command.rb +34 -0
- data/lib/commands/rhino/module/rails_command.rb +43 -0
- data/lib/commands/rhino/module/test_command.rb +43 -0
- data/lib/generators/rhino/dev/setup/setup_generator.rb +199 -0
- data/lib/generators/rhino/dev/setup/templates/env.client.tt +4 -0
- data/lib/generators/rhino/dev/setup/templates/env.root.tt +1 -0
- data/lib/generators/rhino/dev/setup/templates/env.server.tt +35 -0
- data/lib/generators/rhino/dev/setup/templates/prepare-commit-msg +17 -0
- data/lib/generators/rhino/install/install_generator.rb +24 -0
- data/lib/generators/rhino/install/templates/account.rb +4 -0
- data/lib/generators/rhino/install/templates/rhino.rb +24 -0
- data/lib/generators/rhino/install/templates/user.rb +4 -0
- data/lib/generators/rhino/model/model_generator.rb +96 -0
- data/lib/generators/rhino/module/USAGE +6 -0
- data/lib/generators/rhino/module/module_generator.rb +92 -0
- data/lib/generators/rhino/module/templates/%name%.gemspec.tt +24 -0
- data/lib/generators/rhino/module/templates/lib/%namespaced_name%/engine.rb.tt +18 -0
- data/lib/generators/rhino/module/templates/lib/generators/%namespaced_name%/install/install_generator.rb.tt +12 -0
- data/lib/generators/rhino/module/templates/lib/tasks/%namespaced_name%_tasks.rake.tt +13 -0
- data/lib/generators/rhino/module/templates/test/dummy/app/models/user.rb +4 -0
- data/lib/generators/rhino/module/templates/test/dummy/config/database.yml +25 -0
- data/lib/generators/rhino/module/templates/test/dummy/config/initializers/devise.rb +311 -0
- data/lib/generators/rhino/module/templates/test/dummy/config/initializers/devise_token_auth.rb +71 -0
- data/lib/generators/rhino/module/templates/test/test_helper.rb +54 -0
- data/lib/generators/rhino/policy/policy_generator.rb +33 -0
- data/lib/generators/rhino/policy/templates/policy.rb.tt +46 -0
- data/lib/generators/test_unit/rhino_policy_generator.rb +13 -0
- data/lib/generators/test_unit/templates/policy_test.rb.tt +57 -0
- data/lib/rhino/engine.rb +166 -0
- data/lib/rhino/omniauth/strategies/azure_o_auth2.rb +16 -0
- data/lib/rhino/resource/active_model_extension/backing_store/google_sheet.rb +89 -0
- data/lib/rhino/resource/active_model_extension/backing_store.rb +33 -0
- data/lib/rhino/resource/active_model_extension/describe.rb +38 -0
- data/lib/rhino/resource/active_model_extension/params.rb +70 -0
- data/lib/rhino/resource/active_model_extension/properties.rb +231 -0
- data/lib/rhino/resource/active_model_extension/reference.rb +50 -0
- data/lib/rhino/resource/active_model_extension/routing.rb +15 -0
- data/lib/rhino/resource/active_model_extension/serialization.rb +16 -0
- data/lib/rhino/resource/active_model_extension.rb +38 -0
- data/lib/rhino/resource/active_record_extension/describe.rb +44 -0
- data/lib/rhino/resource/active_record_extension/params.rb +213 -0
- data/lib/rhino/resource/active_record_extension/properties.rb +85 -0
- data/lib/rhino/resource/active_record_extension/properties_describe.rb +228 -0
- data/lib/rhino/resource/active_record_extension/reference.rb +50 -0
- data/lib/rhino/resource/active_record_extension/routing.rb +21 -0
- data/lib/rhino/resource/active_record_extension/search.rb +23 -0
- data/lib/rhino/resource/active_record_extension/serialization.rb +16 -0
- data/lib/rhino/resource/active_record_extension/super_admin.rb +25 -0
- data/lib/rhino/resource/active_record_extension.rb +32 -0
- data/lib/rhino/resource/active_record_tree.rb +50 -0
- data/lib/rhino/resource/active_storage_extension.rb +41 -0
- data/lib/rhino/resource/describe.rb +19 -0
- data/lib/rhino/resource/owner.rb +172 -0
- data/lib/rhino/resource/params.rb +31 -0
- data/lib/rhino/resource/properties.rb +192 -0
- data/lib/rhino/resource/reference.rb +29 -0
- data/lib/rhino/resource/routing.rb +107 -0
- data/lib/rhino/resource/serialization.rb +13 -0
- data/lib/rhino/resource/sieves.rb +36 -0
- data/lib/rhino/resource.rb +55 -0
- data/lib/rhino/sieve/filter.rb +149 -0
- data/lib/rhino/sieve/geospatial.rb +45 -0
- data/lib/rhino/sieve/helpers.rb +11 -0
- data/lib/rhino/sieve/limit.rb +20 -0
- data/lib/rhino/sieve/offset.rb +16 -0
- data/lib/rhino/sieve/order.rb +143 -0
- data/lib/rhino/sieve/search.rb +20 -0
- data/lib/rhino/sieve.rb +159 -0
- data/lib/rhino/test_case/controller.rb +145 -0
- data/lib/rhino/test_case/model.rb +86 -0
- data/lib/rhino/test_case/override.rb +19 -0
- data/lib/rhino/test_case/policy.rb +76 -0
- data/lib/rhino/test_case.rb +11 -0
- data/lib/rhino/version.rb +17 -0
- data/lib/rhino_project_core.rb +131 -0
- data/lib/tasks/rhino.rake +24 -0
- data/lib/tasks/rhino_dev.rake +17 -0
- data/lib/validators/country_validator.rb +11 -0
- data/lib/validators/email_validator.rb +8 -0
- data/lib/validators/ipv4_validator.rb +10 -0
- data/lib/validators/mac_address_validator.rb +9 -0
- metadata +531 -0
@@ -0,0 +1,149 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Rhino
|
4
|
+
module Sieve
|
5
|
+
class Filter # rubocop:disable Metrics/ClassLength
|
6
|
+
def initialize(app)
|
7
|
+
@app = app
|
8
|
+
end
|
9
|
+
|
10
|
+
# filter[blog_id]=1
|
11
|
+
def resolve(scope, params)
|
12
|
+
return @app.resolve(scope, params) unless params.key?(:filter)
|
13
|
+
|
14
|
+
filter = params[:filter].permit!.to_h
|
15
|
+
scope = scope.joins(get_joins(scope.klass, filter))
|
16
|
+
query = apply_filters(scope, scope.klass, filter).distinct(:id)
|
17
|
+
@app.resolve(query, params)
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
def get_joins_hash(base, filter)
|
22
|
+
res = []
|
23
|
+
filter.each do |key, val|
|
24
|
+
value_is_hash = val.is_a? Hash
|
25
|
+
# only consider a key for the join clause if it is a relationship a.k.a reflection
|
26
|
+
key_is_reflection = base.reflections.key? key
|
27
|
+
|
28
|
+
next unless value_is_hash && key_is_reflection
|
29
|
+
|
30
|
+
# in the next recursion, the associations will have to be checked against this current key, not the initial model
|
31
|
+
reflection_model = base.reflections[key.to_s].klass
|
32
|
+
res << { key => get_joins_hash(reflection_model, val) }
|
33
|
+
end
|
34
|
+
res
|
35
|
+
end
|
36
|
+
|
37
|
+
def get_joins(base, filter)
|
38
|
+
joins_hash = get_joins_hash(base, filter)
|
39
|
+
result = []
|
40
|
+
joins_hash.each do |entry|
|
41
|
+
entry.each do |key, value|
|
42
|
+
result << ArelHelpers.join_association(base, { key => value }, Arel::Nodes::OuterJoin, {})
|
43
|
+
end
|
44
|
+
end
|
45
|
+
result
|
46
|
+
end
|
47
|
+
|
48
|
+
def apply_filters(scope, base, filter)
|
49
|
+
filter.each do |key, val|
|
50
|
+
key = Sieve::Helpers.real_column_name(scope, key)
|
51
|
+
key_is_reflection = base.reflections.key? key
|
52
|
+
key_is_attribute = base.attribute_names.include? key
|
53
|
+
key_is_association_operator = ASSOCIATION_OPS.include? key
|
54
|
+
if key_is_association_operator
|
55
|
+
scope = apply_association_operator(base, scope, key, val)
|
56
|
+
elsif key_is_reflection || key_is_attribute
|
57
|
+
scope = apply_filter(scope, base, key, val)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
scope
|
61
|
+
end
|
62
|
+
|
63
|
+
def apply_filter(scope, base, key, val)
|
64
|
+
reflection = base.reflections[key.to_s]
|
65
|
+
value_is_hash = val.is_a? Hash
|
66
|
+
if value_is_hash && reflection
|
67
|
+
# joined table filter, continue recursion, e.g. ?filter[blog][...]
|
68
|
+
association_base = reflection.klass
|
69
|
+
apply_filters(scope, association_base, val)
|
70
|
+
elsif reflection
|
71
|
+
# direct association id filter, e.g. ?filter[blog]=1
|
72
|
+
apply_association_filter(base, scope, key, val)
|
73
|
+
else
|
74
|
+
# column filter, e.g. ?filter[name]=...
|
75
|
+
apply_column_filter(base, scope, key, val)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def apply_association_filter(base, scope, key, value)
|
80
|
+
scope.where(base.arel_table[base.reflections[key].foreign_key].eq(value))
|
81
|
+
end
|
82
|
+
|
83
|
+
ASSOCIATION_OPS = %w[_is_empty].freeze
|
84
|
+
def apply_association_operator(base, scope, key, value)
|
85
|
+
if key == '_is_empty' && truthy?(value)
|
86
|
+
arel_node = base.arel_table['id']
|
87
|
+
|
88
|
+
where_clause = arel_node.eq(nil)
|
89
|
+
scope.where(where_clause)
|
90
|
+
else
|
91
|
+
scope
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def apply_column_filter(base, scope, column_name, column_value)
|
96
|
+
if column_value.is_a?(Hash)
|
97
|
+
# it is a more complex query, possibly using operators like gt, lt, etc.
|
98
|
+
# more than one operator per-field is allowed
|
99
|
+
column_value.each do |operation, value|
|
100
|
+
scope = merge_where_clause(base, scope, column_name, value, operation)
|
101
|
+
end
|
102
|
+
else
|
103
|
+
scope = merge_where_clause(base, scope, column_name, column_value)
|
104
|
+
end
|
105
|
+
scope
|
106
|
+
end
|
107
|
+
|
108
|
+
BASIC_AREL_OPS = %w[eq gt lt gteq lteq].freeze
|
109
|
+
BASIC_AREL_COALESCE_OPS = BASIC_AREL_OPS.map { |op| "#{op}_coalesce" }.freeze
|
110
|
+
def merge_where_clause(base, scope, column_name, value, operation = nil) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
|
111
|
+
arel_node = base.arel_table[column_name]
|
112
|
+
where_clause = case operation
|
113
|
+
when *BASIC_AREL_OPS then arel_node.send(operation, value)
|
114
|
+
when *BASIC_AREL_COALESCE_OPS
|
115
|
+
coalesce = Arel::Nodes::NamedFunction.new('COALESCE', [arel_node, Arel::Nodes.build_quoted(value)])
|
116
|
+
coalesce.send(operation.split('_').first, value)
|
117
|
+
when 'diff' then arel_node.not_eq(value)
|
118
|
+
when 'is_null' then apply_is_null(arel_node, value)
|
119
|
+
when /^tree_(.*)/ then apply_tree(Regexp.last_match(1), base, column_name, arel_node, value)
|
120
|
+
else
|
121
|
+
if value.is_a? Array
|
122
|
+
arel_node.in(value)
|
123
|
+
else
|
124
|
+
arel_node.eq(value)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
scope.where(where_clause)
|
128
|
+
end
|
129
|
+
|
130
|
+
def apply_is_null(arel_node, value)
|
131
|
+
if ActiveModel::Type::Boolean::FALSE_VALUES.include?(value)
|
132
|
+
arel_node.not_eq(nil)
|
133
|
+
else
|
134
|
+
arel_node.eq(nil)
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
def apply_tree(operation, base, column_name, arel_node, value)
|
139
|
+
subquery = base.where(column_name => value).map(&operation.to_sym).flatten.map(&column_name.to_sym)
|
140
|
+
|
141
|
+
arel_node.in(subquery)
|
142
|
+
end
|
143
|
+
|
144
|
+
def truthy?(value)
|
145
|
+
value.present? && ActiveModel::Type::Boolean::FALSE_VALUES.exclude?(value)
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Rhino
|
4
|
+
module Sieve
|
5
|
+
class Geospatial
|
6
|
+
def initialize(app)
|
7
|
+
@app = app
|
8
|
+
end
|
9
|
+
|
10
|
+
# geospatial={near: {location: { latitude: 41.2565, longitude: -95.9345 }, max_distance: 10, units: 'km'}}
|
11
|
+
# geospatial={near: {location: 'Omaha, NE, US', max_distance: 10, units: 'km'}}
|
12
|
+
def resolve(scope, params)
|
13
|
+
return @app.resolve(scope, params) unless valid?(params)
|
14
|
+
|
15
|
+
@app.resolve(
|
16
|
+
scope.near(
|
17
|
+
@location_params,
|
18
|
+
@near_params[:max_distance],
|
19
|
+
units: @near_params[:units]
|
20
|
+
), params
|
21
|
+
)
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
def valid?(params) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
26
|
+
@geospatial_params = params[:geospatial]
|
27
|
+
return false unless @geospatial_params.is_a?(ActionController::Parameters)
|
28
|
+
|
29
|
+
@near_params = @geospatial_params[:near]
|
30
|
+
return false unless @near_params.is_a?(ActionController::Parameters)
|
31
|
+
|
32
|
+
@location_params = @near_params[:location]
|
33
|
+
if @location_params.is_a?(String)
|
34
|
+
@location_params = @near_params[:location]
|
35
|
+
elsif @location_params.is_a?(ActionController::Parameters) && @location_params[:latitude].present? && @location_params[:longitude].present?
|
36
|
+
@location_params = [@location_params[:latitude], @location_params[:longitude]]
|
37
|
+
else
|
38
|
+
return false
|
39
|
+
end
|
40
|
+
|
41
|
+
@near_params[:max_distance].present?
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Rhino
|
4
|
+
module Sieve
|
5
|
+
class Limit
|
6
|
+
attr_accessor :default_limit
|
7
|
+
|
8
|
+
def initialize(app, default_limit: 20)
|
9
|
+
@app = app
|
10
|
+
|
11
|
+
@default_limit = default_limit
|
12
|
+
end
|
13
|
+
|
14
|
+
# limit=20
|
15
|
+
def resolve(scope, params)
|
16
|
+
@app.resolve(scope.limit(params[:limit] || default_limit), params)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,143 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Rhino
|
4
|
+
module Sieve
|
5
|
+
class Order
|
6
|
+
def initialize(app)
|
7
|
+
@app = app
|
8
|
+
end
|
9
|
+
|
10
|
+
# order=name
|
11
|
+
def resolve(scope, params)
|
12
|
+
@scope = scope
|
13
|
+
@param = params[:order]
|
14
|
+
|
15
|
+
# Always append id to the end of the order clause to ensure a stable sort for pagination
|
16
|
+
result = apply_order.order(:id)
|
17
|
+
@app.resolve(result, params)
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
def parse(param)
|
22
|
+
direction = parse_direction(param)
|
23
|
+
column_name = parse_column_name(param, direction)
|
24
|
+
[direction, column_name]
|
25
|
+
end
|
26
|
+
|
27
|
+
def parse_direction(param)
|
28
|
+
if param[0] == '-'
|
29
|
+
:desc
|
30
|
+
else
|
31
|
+
:asc
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def parse_column_name(param, direction)
|
36
|
+
param = if direction == :asc
|
37
|
+
param
|
38
|
+
else
|
39
|
+
param[1..]
|
40
|
+
end
|
41
|
+
Sieve::Helpers.real_column_name(@scope, param)
|
42
|
+
end
|
43
|
+
|
44
|
+
def string?(param)
|
45
|
+
param.is_a? String
|
46
|
+
end
|
47
|
+
|
48
|
+
def get_joins(base, path, _last_base)
|
49
|
+
return {} if path.empty?
|
50
|
+
|
51
|
+
# Convert to nested hash
|
52
|
+
join_tables = path.reverse.inject({}) { |assigned_value, key| { key => assigned_value } }
|
53
|
+
|
54
|
+
# If InnerJoin was used instead of OuterJoin, a simple ORDER clause targeting an association's column
|
55
|
+
# would incur in a join being made, which in turn would exclude records with no association. This would
|
56
|
+
# be conceptually wrong, as an ORDER clause would be excluding records from the final result. In order to
|
57
|
+
# restore the previous behavior and exclude these records without parent, one could simply add a WHERE
|
58
|
+
# clause with something like `WHERE association.id IS NOT NULL`.
|
59
|
+
ArelHelpers.join_association(base, join_tables, Arel::Nodes::OuterJoin, {})
|
60
|
+
rescue StandardError
|
61
|
+
nil
|
62
|
+
end
|
63
|
+
|
64
|
+
def build_select_clause(model, path, field)
|
65
|
+
return model.arel_table[Arel.star] if path.empty?
|
66
|
+
|
67
|
+
model.arel_table[field]
|
68
|
+
end
|
69
|
+
|
70
|
+
def split_field_path(column_name)
|
71
|
+
chunks = column_name.split('.')
|
72
|
+
field = chunks.last
|
73
|
+
path = chunks.slice(0, chunks.length - 1)
|
74
|
+
[path, field]
|
75
|
+
end
|
76
|
+
|
77
|
+
def analyze(column_name)
|
78
|
+
path, field = split_field_path(column_name)
|
79
|
+
|
80
|
+
last_base = if path.last.blank?
|
81
|
+
@scope
|
82
|
+
else
|
83
|
+
path.last.classify.safe_constantize
|
84
|
+
end
|
85
|
+
return nil unless last_base&.attribute_names&.include? field
|
86
|
+
|
87
|
+
join_clauses = get_joins(@scope.klass, path, last_base)
|
88
|
+
select_clause = build_select_clause(last_base, path, field)
|
89
|
+
|
90
|
+
[join_clauses, last_base.arel_table[field], select_clause]
|
91
|
+
end
|
92
|
+
|
93
|
+
NULL_ORDERING = {
|
94
|
+
asc: :nulls_last,
|
95
|
+
desc: :nulls_first
|
96
|
+
}.freeze
|
97
|
+
def order(direction, column_name)
|
98
|
+
# nulls_last should generally be the desired user experience
|
99
|
+
join_clauses, order_clause, select_clause = analyze(column_name)
|
100
|
+
return nil unless join_clauses && order_clause
|
101
|
+
|
102
|
+
final_clause = order_clause
|
103
|
+
.send(direction)
|
104
|
+
.send(NULL_ORDERING[direction])
|
105
|
+
[join_clauses, final_clause, select_clause]
|
106
|
+
end
|
107
|
+
|
108
|
+
def build_clause(param)
|
109
|
+
return nil unless string?(param)
|
110
|
+
|
111
|
+
direction, column_name = parse(param)
|
112
|
+
order(direction, column_name)
|
113
|
+
end
|
114
|
+
|
115
|
+
def group_clauses(clauses)
|
116
|
+
join_clauses = clauses.map { |joins, _node, _select| joins }
|
117
|
+
order_clauses = clauses.map { |_joins, node, _select| node }
|
118
|
+
select_clauses = clauses.map { |_joins, _node, select| select }
|
119
|
+
[order_clauses, join_clauses, select_clauses]
|
120
|
+
end
|
121
|
+
|
122
|
+
def build_clauses
|
123
|
+
clauses = @param.split(',')
|
124
|
+
.map { |el| build_clause(el) }
|
125
|
+
.filter { |joins, node, select| joins && node && select }
|
126
|
+
group_clauses(clauses)
|
127
|
+
end
|
128
|
+
|
129
|
+
def apply_order
|
130
|
+
return @scope unless string?(@param)
|
131
|
+
|
132
|
+
order_clauses, join_clauses, select_clauses = build_clauses
|
133
|
+
if order_clauses.empty?
|
134
|
+
@scope
|
135
|
+
else
|
136
|
+
select_clauses << "#{@scope.klass.table_name}.*"
|
137
|
+
@scope = @scope.reorder(nil).select(select_clauses.uniq).joins(join_clauses)
|
138
|
+
order_clauses.inject(@scope) { |acc, el| acc.order(el) }
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Rhino
|
4
|
+
module Sieve
|
5
|
+
class Search
|
6
|
+
def initialize(app)
|
7
|
+
@app = app
|
8
|
+
end
|
9
|
+
|
10
|
+
# search='test'
|
11
|
+
def resolve(scope, params)
|
12
|
+
return @app.resolve(scope, params) unless scope.respond_to?(:search_text_fields) && params[:search].present?
|
13
|
+
|
14
|
+
# Re-order so that rank isn't the default
|
15
|
+
# FIXME: Should look at the order param for 'rank'
|
16
|
+
@app.resolve(scope.search_text_fields(params[:search]).reorder(''), params)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
data/lib/rhino/sieve.rb
ADDED
@@ -0,0 +1,159 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Rhino
|
4
|
+
module Sieve
|
5
|
+
extend ActiveSupport::Autoload
|
6
|
+
|
7
|
+
autoload :Filter
|
8
|
+
autoload :Geospatial
|
9
|
+
autoload :Limit
|
10
|
+
autoload :Offset
|
11
|
+
autoload :Order
|
12
|
+
autoload :Search
|
13
|
+
autoload :Helpers
|
14
|
+
end
|
15
|
+
|
16
|
+
class SieveStack
|
17
|
+
class Sieve
|
18
|
+
attr_reader :args, :block, :klass
|
19
|
+
|
20
|
+
def initialize(klass, args, block)
|
21
|
+
@klass = klass
|
22
|
+
@args = args
|
23
|
+
@block = block
|
24
|
+
end
|
25
|
+
|
26
|
+
delegate :name, to: :klass
|
27
|
+
|
28
|
+
def ==(other)
|
29
|
+
case other
|
30
|
+
when Sieve
|
31
|
+
klass == other.klass
|
32
|
+
when Class
|
33
|
+
klass == other
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def inspect
|
38
|
+
if klass.is_a?(Class)
|
39
|
+
klass.to_s
|
40
|
+
else
|
41
|
+
klass.class.to_s
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def build(app)
|
46
|
+
klass.new(app, *args, &block)
|
47
|
+
end
|
48
|
+
|
49
|
+
def build_instrumented(app)
|
50
|
+
InstrumentationProxy.new(build(app), inspect)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# This class is used to instrument the execution of a single sieve.
|
55
|
+
# It proxies the `s` method transparently and instruments the method
|
56
|
+
# call.
|
57
|
+
class InstrumentationProxy
|
58
|
+
EVENT_NAME = 'rhino.apply_sieve'
|
59
|
+
|
60
|
+
def initialize(sieve, class_name)
|
61
|
+
@sieve = sieve
|
62
|
+
|
63
|
+
@payload = {
|
64
|
+
sieve: class_name
|
65
|
+
}
|
66
|
+
end
|
67
|
+
|
68
|
+
def resolve(scope, params)
|
69
|
+
ActiveSupport::Notifications.instrument(EVENT_NAME, @payload) do
|
70
|
+
@sieve.resolve(scope, params)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
# Pretty much stolen from ActionDispatch::MiddlewareStack
|
76
|
+
include Enumerable
|
77
|
+
|
78
|
+
attr_accessor :sieves
|
79
|
+
|
80
|
+
def initialize(*_args)
|
81
|
+
@sieves = []
|
82
|
+
yield(self) if block_given?
|
83
|
+
end
|
84
|
+
|
85
|
+
# rubocop:todo Style/ExplicitBlockArgument
|
86
|
+
def each
|
87
|
+
@sieves.each { |x| yield x }
|
88
|
+
end
|
89
|
+
# rubocop:enable Style/ExplicitBlockArgument
|
90
|
+
|
91
|
+
delegate :[], :size, :last, to: :sieves
|
92
|
+
|
93
|
+
def unshift(klass, *args, &block)
|
94
|
+
sieves.unshift(build_sieve(klass, args, block))
|
95
|
+
end
|
96
|
+
ruby2_keywords(:unshift) if respond_to?(:ruby2_keywords, true)
|
97
|
+
|
98
|
+
def initialize_copy(other)
|
99
|
+
self.sieves = other.sieves.dup
|
100
|
+
end
|
101
|
+
|
102
|
+
def insert(index, klass, *args, &block)
|
103
|
+
index = assert_index(index, :before)
|
104
|
+
sieves.insert(index, build_sieve(klass, args, block))
|
105
|
+
end
|
106
|
+
ruby2_keywords(:insert) if respond_to?(:ruby2_keywords, true)
|
107
|
+
|
108
|
+
alias insert_before insert
|
109
|
+
|
110
|
+
def insert_after(index, *args, &block)
|
111
|
+
index = assert_index(index, :after)
|
112
|
+
insert(index + 1, *args, &block)
|
113
|
+
end
|
114
|
+
ruby2_keywords(:insert_after) if respond_to?(:ruby2_keywords, true)
|
115
|
+
|
116
|
+
def swap(target, *args, &block)
|
117
|
+
index = assert_index(target, :before)
|
118
|
+
insert(index, *args, &block)
|
119
|
+
sieves.delete_at(index + 1)
|
120
|
+
end
|
121
|
+
ruby2_keywords(:swap) if respond_to?(:ruby2_keywords, true)
|
122
|
+
|
123
|
+
def delete(target)
|
124
|
+
sieves.delete_if { |m| m.klass == target }
|
125
|
+
end
|
126
|
+
|
127
|
+
def use(klass, *args, &block)
|
128
|
+
sieves.push(build_sieve(klass, args, block))
|
129
|
+
end
|
130
|
+
ruby2_keywords(:use) if respond_to?(:ruby2_keywords, true)
|
131
|
+
|
132
|
+
def build(app = nil, &block)
|
133
|
+
instrumenting = ActiveSupport::Notifications.notifier.listening?(InstrumentationProxy::EVENT_NAME)
|
134
|
+
|
135
|
+
# Freeze only production so that reloading works
|
136
|
+
sieves.freeze unless Rails.env.development?
|
137
|
+
|
138
|
+
sieves.reverse.inject(app || block) do |a, e|
|
139
|
+
if instrumenting
|
140
|
+
e.build_instrumented(a)
|
141
|
+
else
|
142
|
+
e.build(a)
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
private
|
148
|
+
def assert_index(index, where)
|
149
|
+
i = index.is_a?(Integer) ? index : sieves.index { |m| m.klass == index }
|
150
|
+
raise "No such sieve to insert #{where}: #{index.inspect}" unless i
|
151
|
+
|
152
|
+
i
|
153
|
+
end
|
154
|
+
|
155
|
+
def build_sieve(klass, args, block)
|
156
|
+
Sieve.new(klass, args, block)
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|