wallaby-active_record 0.2.7 → 0.3.0.beta1
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 +4 -4
- data/README.md +13 -2
- data/lib/adapters/wallaby/active_record/model_decorator/fields_builder/sti_builder.rb +1 -1
- data/lib/adapters/wallaby/active_record/model_decorator/fields_builder.rb +35 -17
- data/lib/adapters/wallaby/active_record/model_decorator/title_field_finder.rb +1 -1
- data/lib/adapters/wallaby/active_record/model_decorator.rb +41 -36
- data/lib/adapters/wallaby/active_record/model_finder.rb +5 -1
- data/lib/adapters/wallaby/active_record/model_pagination_provider.rb +2 -2
- data/lib/adapters/wallaby/active_record/model_service_provider/normalizer.rb +6 -5
- data/lib/adapters/wallaby/active_record/model_service_provider/permitter.rb +5 -5
- data/lib/adapters/wallaby/active_record/model_service_provider/querier/transformer.rb +2 -0
- data/lib/adapters/wallaby/active_record/model_service_provider/querier/wrapper.rb +1 -0
- data/lib/adapters/wallaby/active_record/model_service_provider/querier.rb +84 -33
- data/lib/adapters/wallaby/active_record/model_service_provider/validator.rb +1 -1
- data/lib/adapters/wallaby/active_record/model_service_provider.rb +23 -24
- data/lib/wallaby/active_record/version.rb +1 -1
- metadata +10 -9
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 90fb0c2b0c23402987290d4b290b953de709949ffffcbfc1255e8ef7c5efd653
|
|
4
|
+
data.tar.gz: 67d1cbd6394ed22ff8db981b043d10c719992175630e8e0bcc38d5eec4e42acd
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 678e44a61fe8bd550d24924c95fe4b52a51a8e5667f93e46efadb337613e14a99d7befef28d85f2de8d988b6fcf11dc91577640de4246edebb8fba778a639772
|
|
7
|
+
data.tar.gz: 610ef0709fd1c48f770fa2414ba7d64b51b8bc3410b12453a8d3c9a6c1c10fbb62ddb65cd0cd5768415eb8bb2554def31c71a7ad1c11a41490e3cf408466efd1
|
data/README.md
CHANGED
|
@@ -7,8 +7,19 @@
|
|
|
7
7
|
[](https://codeclimate.com/github/wallaby-rails/wallaby-active_record/test_coverage)
|
|
8
8
|
[](https://inch-ci.org/github/wallaby-rails/wallaby-active_record)
|
|
9
9
|
|
|
10
|
-
Wallaby::ActiveRecord is
|
|
11
|
-
interfaces
|
|
10
|
+
`Wallaby::ActiveRecord` adapter is designed to seamlessly integrate ActiveRecord models and instances with the [Wallaby::Core](https://github.com/wallaby-rails/wallaby-core)
|
|
11
|
+
interfaces, enabling smooth handling of CRUD operations, authorization, and pagination.
|
|
12
|
+
|
|
13
|
+
Key features of `Wallaby::ActiveRecord` include:
|
|
14
|
+
|
|
15
|
+
- Offer built-in compatibility with popular authorization frameworks like CanCanCan and Pundit, allowing you to easily manage access control for your ActiveRecord models.
|
|
16
|
+
- Fully support all Rails associations (`belongs_to`, `has_one`, `has_many`, `has_and_belongs_to_many`).
|
|
17
|
+
- All PostgreSQL types supported by Rails are fully supported to ensure smooth handling of data with different types.
|
|
18
|
+
- Facilitate the normalization of data during form submissions, specifically for `binary`, `point` and `range` types.
|
|
19
|
+
- Provide the flexibility to define custom filters for the `index` action, enabling you to create tailored queries based on your specific requirements.
|
|
20
|
+
- Allow sorting for single or multiple columns and provide `nulls` sorting option, enhancing the control and precision of result ordering.
|
|
21
|
+
- Support STI (Single Table Inheritance)
|
|
22
|
+
- Offer advance search capabilities using a convenient colon syntax, empowering users to perform precise searches with ease.
|
|
12
23
|
|
|
13
24
|
## Install
|
|
14
25
|
|
|
@@ -29,7 +29,7 @@ module Wallaby
|
|
|
29
29
|
# @param klass [Class]
|
|
30
30
|
# @return [Array<Class>]
|
|
31
31
|
def sti_list(klass)
|
|
32
|
-
(klass.descendants << klass).sort_by(&:name)
|
|
32
|
+
(klass.descendants << klass).sort_by(&:name).uniq
|
|
33
33
|
end
|
|
34
34
|
|
|
35
35
|
# Find out which parent is the one that can give us the STI list.
|
|
@@ -12,28 +12,31 @@ module Wallaby
|
|
|
12
12
|
|
|
13
13
|
# @return [Hash<String, Hash>] a hash for general fields
|
|
14
14
|
def general_fields
|
|
15
|
-
@
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
15
|
+
@general_fields ||=
|
|
16
|
+
@model_class.columns.each_with_object({}) do |column, fields|
|
|
17
|
+
metadata = {
|
|
18
|
+
type: to_type(column).freeze,
|
|
19
|
+
label: @model_class.human_attribute_name(column.name)
|
|
20
|
+
}
|
|
21
|
+
sti_builder.update(metadata, column)
|
|
22
|
+
fields[column.name] = metadata
|
|
23
|
+
end
|
|
23
24
|
end
|
|
24
25
|
|
|
25
26
|
# @return [Hash<String, Hash>] a hash for association fields
|
|
26
27
|
# (e.g. belongs_to / has_one / has_many / has_and_belongs_to_many)
|
|
27
28
|
def association_fields
|
|
28
|
-
@
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
29
|
+
@association_fields ||=
|
|
30
|
+
@model_class.reflections.each_with_object({}) do |(name, reflection), fields|
|
|
31
|
+
metadata = {
|
|
32
|
+
type: reflection.macro.to_s, # association type
|
|
33
|
+
label: @model_class.human_attribute_name(name)
|
|
34
|
+
}
|
|
35
|
+
association_builder.update(metadata, reflection)
|
|
36
|
+
polymorphic_builder.update(metadata, reflection)
|
|
37
|
+
update_general_fields_with(metadata)
|
|
38
|
+
fields[name] = metadata
|
|
39
|
+
end
|
|
37
40
|
end
|
|
38
41
|
|
|
39
42
|
protected
|
|
@@ -61,6 +64,21 @@ module Wallaby
|
|
|
61
64
|
def polymorphic_builder
|
|
62
65
|
@polymorphic_builder ||= PolymorphicBuilder.new
|
|
63
66
|
end
|
|
67
|
+
|
|
68
|
+
# @param metadata [Hash] association metadata
|
|
69
|
+
def update_general_fields_with(metadata)
|
|
70
|
+
metadata[:foreign_key].try do |key|
|
|
71
|
+
general_fields[key].try do |general_metadata|
|
|
72
|
+
general_metadata[:is_foreign_key] = general_metadata[:hidden] = true
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
metadata[:polymorphic_type].try do |key|
|
|
77
|
+
general_fields[key].try do |general_metadata|
|
|
78
|
+
general_metadata[:is_polymorphic_type] = general_metadata[:hidden] = true
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
64
82
|
end
|
|
65
83
|
end
|
|
66
84
|
end
|
|
@@ -5,7 +5,7 @@ module Wallaby
|
|
|
5
5
|
class ModelDecorator
|
|
6
6
|
# Try to find the field that can be used as title
|
|
7
7
|
class TitleFieldFinder
|
|
8
|
-
TITLE_FIELD_TYPES = %w
|
|
8
|
+
TITLE_FIELD_TYPES = %w[string].freeze
|
|
9
9
|
|
|
10
10
|
# @param model_class [Class]
|
|
11
11
|
# @param fields [Hash] fields metadata
|
|
@@ -5,16 +5,16 @@ module Wallaby
|
|
|
5
5
|
# Modal decorator for {Wallaby::ActiveRecord}
|
|
6
6
|
class ModelDecorator < ::Wallaby::ModelDecorator
|
|
7
7
|
# Data types to exclude for {#index_field_names}
|
|
8
|
-
INDEX_EXCLUSIVE_DATA_TYPES = %w
|
|
8
|
+
INDEX_EXCLUSIVE_DATA_TYPES = %w[
|
|
9
9
|
binary citext hstore json jsonb tsvector xml
|
|
10
10
|
blob mediumblob longblob text mediumtext longtext
|
|
11
|
-
|
|
11
|
+
].freeze
|
|
12
12
|
|
|
13
13
|
# Classes to exclude for {#show_field_names}
|
|
14
|
-
SHOW_EXCLUSIVE_CLASS_NAMES = %w
|
|
14
|
+
SHOW_EXCLUSIVE_CLASS_NAMES = %w[ActiveStorage::Attachment ActiveStorage::Blob].freeze
|
|
15
15
|
|
|
16
16
|
# Fields to exclude for {#form_field_names}
|
|
17
|
-
FORM_EXCLUSIVE_DATA_TYPES = %w
|
|
17
|
+
FORM_EXCLUSIVE_DATA_TYPES = %w[created_at updated_at].freeze
|
|
18
18
|
|
|
19
19
|
# Original metadata information of the primative and association fields
|
|
20
20
|
# pulling out from the ActiveRecord model.
|
|
@@ -45,46 +45,49 @@ module Wallaby
|
|
|
45
45
|
@fields ||= ::ActiveSupport::HashWithIndifferentAccess.new.tap do |hash|
|
|
46
46
|
next hash.default = {} unless @model_class.table_exists?
|
|
47
47
|
|
|
48
|
-
hash.merge!
|
|
49
|
-
hash.merge!
|
|
50
|
-
|
|
48
|
+
hash.merge!(association_fields)
|
|
49
|
+
hash.merge!(general_fields)
|
|
50
|
+
rescue ::ActiveRecord::NoDatabaseError
|
|
51
|
+
hash.default = {}
|
|
51
52
|
end.freeze
|
|
52
|
-
rescue ::ActiveRecord::NoDatabaseError
|
|
53
|
-
Hash.new({}).with_indifferent_access
|
|
54
53
|
end
|
|
55
54
|
|
|
56
55
|
# A copy of {#fields} for index page
|
|
57
56
|
# @return [ActiveSupport::HashWithIndifferentAccess] metadata
|
|
58
57
|
def index_fields
|
|
59
|
-
@index_fields ||= Utils.clone
|
|
58
|
+
@index_fields ||= Utils.clone(fields)
|
|
60
59
|
end
|
|
61
60
|
|
|
62
61
|
# A copy of {#fields} for show page
|
|
63
62
|
# @return [ActiveSupport::HashWithIndifferentAccess] metadata
|
|
64
63
|
def show_fields
|
|
65
|
-
@show_fields ||= Utils.clone
|
|
64
|
+
@show_fields ||= Utils.clone(fields)
|
|
66
65
|
end
|
|
67
66
|
|
|
68
67
|
# A copy of {#fields} for form (new/edit) page
|
|
69
68
|
# @return [ActiveSupport::HashWithIndifferentAccess] metadata
|
|
70
69
|
def form_fields
|
|
71
|
-
@form_fields ||= Utils.clone
|
|
70
|
+
@form_fields ||= Utils.clone(fields)
|
|
72
71
|
end
|
|
73
72
|
|
|
74
73
|
# @return [Array<String>] a list of field names for index page (note: only primitive SQL types are included).
|
|
75
74
|
def index_field_names
|
|
76
75
|
@index_field_names ||=
|
|
77
|
-
|
|
78
|
-
metadata
|
|
79
|
-
|
|
80
|
-
|
|
76
|
+
reposition(
|
|
77
|
+
index_fields.reject do |_field_name, metadata|
|
|
78
|
+
metadata[:hidden] || # e.g. foreign keys, polymorphic columns
|
|
79
|
+
metadata[:is_association] || # associations
|
|
80
|
+
INDEX_EXCLUSIVE_DATA_TYPES.include?(metadata[:type]) # not the types for index page
|
|
81
|
+
end.keys
|
|
82
|
+
)
|
|
81
83
|
end
|
|
82
84
|
|
|
83
85
|
# @return [Array<String>] a list of field names for show page (note: **ActiveStorage** fields are excluded).
|
|
84
86
|
def show_field_names
|
|
85
87
|
@show_field_names ||=
|
|
86
88
|
show_fields.reject do |_field_name, metadata|
|
|
87
|
-
|
|
89
|
+
metadata[:hidden] || # e.g. foreign keys, polymorphic columns
|
|
90
|
+
SHOW_EXCLUSIVE_CLASS_NAMES.include?(metadata[:class].try(:name)) # not the types for show page
|
|
88
91
|
end.keys
|
|
89
92
|
end
|
|
90
93
|
|
|
@@ -93,9 +96,11 @@ module Wallaby
|
|
|
93
96
|
def form_field_names
|
|
94
97
|
@form_field_names ||=
|
|
95
98
|
form_fields.reject do |field_name, metadata|
|
|
96
|
-
field_name == primary_key
|
|
97
|
-
||
|
|
98
|
-
|
|
99
|
+
field_name == primary_key || # primary key
|
|
100
|
+
metadata[:has_scope] || # not a regular association
|
|
101
|
+
metadata[:is_through] || # not direct association
|
|
102
|
+
metadata[:hidden] || # e.g. foreign keys, polymorphic columns
|
|
103
|
+
FORM_EXCLUSIVE_DATA_TYPES.include?(field_name) # not the types for form page
|
|
99
104
|
end.keys
|
|
100
105
|
end
|
|
101
106
|
|
|
@@ -106,7 +111,17 @@ module Wallaby
|
|
|
106
111
|
|
|
107
112
|
# @return [String] primary key for the resource
|
|
108
113
|
def primary_key
|
|
109
|
-
@primary_key ||=
|
|
114
|
+
@primary_key ||=
|
|
115
|
+
@model_class.primary_key || Wallaby::Logger.warn(<<~WARNING)
|
|
116
|
+
No primary key is found and all resource pages (show/edit) will fail to build.
|
|
117
|
+
|
|
118
|
+
If the resource pages are needed, try:
|
|
119
|
+
|
|
120
|
+
- Set `self.primary_key=` in the model #{@model_class}
|
|
121
|
+
- Set `self.primary_key=` in the decorator
|
|
122
|
+
|
|
123
|
+
Otherwise, configure to disable access to new/show/edit/destroy
|
|
124
|
+
WARNING
|
|
110
125
|
end
|
|
111
126
|
|
|
112
127
|
# To guess the title for resource.
|
|
@@ -117,20 +132,21 @@ module Wallaby
|
|
|
117
132
|
# @param resource [Object]
|
|
118
133
|
# @return [String] the title of given resource
|
|
119
134
|
def guess_title(resource)
|
|
120
|
-
|
|
135
|
+
title_field_finder.find.try do |title_method|
|
|
136
|
+
resource.try(title_method)
|
|
137
|
+
end
|
|
121
138
|
end
|
|
122
139
|
|
|
123
140
|
protected
|
|
124
141
|
|
|
125
142
|
# @return [Wallaby::ActiveRecord::ModelDecorator::FieldsBuilder]
|
|
126
143
|
def field_builder
|
|
127
|
-
@field_builder ||= FieldsBuilder.new
|
|
144
|
+
@field_builder ||= FieldsBuilder.new(@model_class)
|
|
128
145
|
end
|
|
129
146
|
|
|
130
147
|
# @return [Wallaby::ActiveRecord::ModelDecorator::TitleFieldFinder]
|
|
131
148
|
def title_field_finder
|
|
132
|
-
@title_field_finder ||=
|
|
133
|
-
TitleFieldFinder.new @model_class, general_fields
|
|
149
|
+
@title_field_finder ||= TitleFieldFinder.new(@model_class, general_fields)
|
|
134
150
|
end
|
|
135
151
|
|
|
136
152
|
# @!method general_fields
|
|
@@ -141,17 +157,6 @@ module Wallaby
|
|
|
141
157
|
# (see Wallaby::ActiveRecord::ModelDecorator::FieldsBuilder#association_fields)
|
|
142
158
|
# @see Wallaby::ActiveRecord::ModelDecorator::FieldsBuilder#association_fields
|
|
143
159
|
delegate :general_fields, :association_fields, to: :field_builder
|
|
144
|
-
|
|
145
|
-
# Find out all the foreign keys for association fields
|
|
146
|
-
# @param fields [Hash] metadata of fields
|
|
147
|
-
# @return [Array<String>] a list of foreign keys
|
|
148
|
-
def foreign_keys_from_associations(fields = association_fields)
|
|
149
|
-
fields.each_with_object([]) do |(_field_name, metadata), keys|
|
|
150
|
-
keys << metadata[:foreign_key] if metadata[:foreign_key]
|
|
151
|
-
keys << metadata[:polymorphic_type] if metadata[:polymorphic_type]
|
|
152
|
-
keys
|
|
153
|
-
end
|
|
154
|
-
end
|
|
155
160
|
end
|
|
156
161
|
end
|
|
157
162
|
end
|
|
@@ -12,7 +12,7 @@ module Wallaby
|
|
|
12
12
|
# @return [Array<Class>]
|
|
13
13
|
def all
|
|
14
14
|
::ActiveRecord::Base.descendants.reject do |model_class|
|
|
15
|
-
|
|
15
|
+
application_record?(model_class) ||
|
|
16
16
|
model_class.abstract_class? ||
|
|
17
17
|
anonymous?(model_class) ||
|
|
18
18
|
model_class.name.index('HABTM') ||
|
|
@@ -22,6 +22,10 @@ module Wallaby
|
|
|
22
22
|
|
|
23
23
|
protected
|
|
24
24
|
|
|
25
|
+
def application_record?(model_class)
|
|
26
|
+
defined?(::ApplicationRecord) && model_class == ::ApplicationRecord
|
|
27
|
+
end
|
|
28
|
+
|
|
25
29
|
# @param model_class [Class]
|
|
26
30
|
# @see Wallaby::ModuleUtils.anonymous_class?
|
|
27
31
|
def anonymous?(model_class)
|
|
@@ -15,12 +15,12 @@ module Wallaby
|
|
|
15
15
|
|
|
16
16
|
# @return [Integer] total count for the collection
|
|
17
17
|
def total
|
|
18
|
-
@collection.unscope(:offset, :limit).count
|
|
18
|
+
@collection.unscope(:offset, :limit).count # rubocop:disable CodeReuse/ActiveRecord
|
|
19
19
|
end
|
|
20
20
|
|
|
21
21
|
# @return [Integer] page size from parameters or Wallaby configuration
|
|
22
22
|
def page_size
|
|
23
|
-
|
|
23
|
+
@params[:per].to_i
|
|
24
24
|
end
|
|
25
25
|
|
|
26
26
|
# @return [Integer] page number from parameters starting from 1
|
|
@@ -14,11 +14,12 @@ module Wallaby
|
|
|
14
14
|
def normalize(params)
|
|
15
15
|
params.each do |field_name, values|
|
|
16
16
|
metadata =
|
|
17
|
-
@model_decorator.metadata_of(field_name).presence ||
|
|
17
|
+
@model_decorator.metadata_of(field_name).presence ||
|
|
18
|
+
@model_decorator.form_metadata_of(field_name)
|
|
18
19
|
type = metadata[:type].try(:[], /range|point|binary/)
|
|
19
20
|
next unless type
|
|
20
21
|
|
|
21
|
-
|
|
22
|
+
try "normalize_#{type}_values", params, field_name, values
|
|
22
23
|
end
|
|
23
24
|
params
|
|
24
25
|
end
|
|
@@ -29,7 +30,7 @@ module Wallaby
|
|
|
29
30
|
# @param values [Array]
|
|
30
31
|
def normalize_range_values(params, field_name, values)
|
|
31
32
|
normalized = Array(values).map(&:presence).compact
|
|
32
|
-
params[field_name] = (normalized.present? && values.length == 2) && (values.first...values.last) || nil
|
|
33
|
+
params[field_name] = ((normalized.present? && values.length == 2) && (values.first...values.last)) || nil
|
|
33
34
|
end
|
|
34
35
|
|
|
35
36
|
# Turn values into points
|
|
@@ -38,7 +39,7 @@ module Wallaby
|
|
|
38
39
|
# @param values [Array]
|
|
39
40
|
def normalize_point_values(params, field_name, values)
|
|
40
41
|
normalized = Array(values).map(&:presence).compact
|
|
41
|
-
params[field_name] = normalized.present? && values.map(&:to_f) || nil
|
|
42
|
+
params[field_name] = (normalized.present? && values.map(&:to_f)) || nil
|
|
42
43
|
end
|
|
43
44
|
|
|
44
45
|
# Turn values into binary
|
|
@@ -46,7 +47,7 @@ module Wallaby
|
|
|
46
47
|
# @param field_name [String]
|
|
47
48
|
# @param values [Object]
|
|
48
49
|
def normalize_binary_values(params, field_name, values)
|
|
49
|
-
params[field_name] = values.is_a?(::ActionDispatch::Http::UploadedFile) && values.read || nil
|
|
50
|
+
params[field_name] = (values.is_a?(::ActionDispatch::Http::UploadedFile) && values.read) || nil
|
|
50
51
|
end
|
|
51
52
|
end
|
|
52
53
|
end
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
module Wallaby
|
|
4
4
|
class ActiveRecord
|
|
5
5
|
class ModelServiceProvider
|
|
6
|
-
#
|
|
6
|
+
# Allowlist the params for mass-assignment
|
|
7
7
|
class Permitter
|
|
8
8
|
# @param model_decorator [Wallaby::ModelDecorator]
|
|
9
9
|
def initialize(model_decorator)
|
|
@@ -14,11 +14,11 @@ module Wallaby
|
|
|
14
14
|
def simple_field_names
|
|
15
15
|
field_names =
|
|
16
16
|
non_range_fields.keys +
|
|
17
|
-
belongs_to_fields.
|
|
17
|
+
belongs_to_fields.flat_map do |_, metadata|
|
|
18
18
|
[metadata[:foreign_key], metadata[:polymorphic_type]]
|
|
19
|
-
end.
|
|
19
|
+
end.compact
|
|
20
20
|
fields = [@model_decorator.primary_key, 'created_at', 'updated_at']
|
|
21
|
-
field_names.reject { |field_name| fields.include?
|
|
21
|
+
field_names.reject { |field_name| fields.include?(field_name) }.uniq
|
|
22
22
|
end
|
|
23
23
|
|
|
24
24
|
# @return [Array<String>] a list of field names of range and association
|
|
@@ -55,7 +55,7 @@ module Wallaby
|
|
|
55
55
|
# - has_many
|
|
56
56
|
# - has_and_belongs_to_many
|
|
57
57
|
def many_association_fields
|
|
58
|
-
association_fields.select { |_, metadata|
|
|
58
|
+
association_fields.select { |_, metadata| metadata[:type].include?('many') }
|
|
59
59
|
end
|
|
60
60
|
|
|
61
61
|
# @return [Array<String>] a list of belongs_to association field names
|
|
@@ -5,7 +5,7 @@ module Wallaby
|
|
|
5
5
|
class ModelServiceProvider
|
|
6
6
|
# Query builder
|
|
7
7
|
class Querier
|
|
8
|
-
TEXT_FIELDS = %w
|
|
8
|
+
TEXT_FIELDS = %w[string text citext longtext tinytext mediumtext].freeze
|
|
9
9
|
|
|
10
10
|
# @param model_decorator [Wallaby::ModelDecorator]
|
|
11
11
|
def initialize(model_decorator)
|
|
@@ -19,11 +19,26 @@ module Wallaby
|
|
|
19
19
|
# @param params [ActionController::Parameters]
|
|
20
20
|
# @return [ActiveRecord::Relation]
|
|
21
21
|
def search(params)
|
|
22
|
-
filter_name, keywords, field_queries = extract
|
|
23
|
-
scope = filtered_by
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
22
|
+
filter_name, keywords, field_queries = extract(params)
|
|
23
|
+
scope = filtered_by(filter_name)
|
|
24
|
+
scope = preload_associations_for(scope)
|
|
25
|
+
arel_query = text_search(keywords)
|
|
26
|
+
arel_query = field_search(field_queries, arel_query)
|
|
27
|
+
scope.where(arel_query) # rubocop:disable CodeReuse/ActiveRecord
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Extract the sorting from parameters `sort`,
|
|
31
|
+
# and combine with metadata option `:nulls`
|
|
32
|
+
# to execute order for the given scope.
|
|
33
|
+
# @param sort_string [String]
|
|
34
|
+
# @param scope [ActiveRecord::Relation]
|
|
35
|
+
# @return [ActiveRecord::Relation]
|
|
36
|
+
def sort(sort_string, scope)
|
|
37
|
+
return scope if sort_string.blank?
|
|
38
|
+
|
|
39
|
+
sort_hash = normalize_sort(Sorting::HashBuilder.build(sort_string))
|
|
40
|
+
sorting = Sorting::HashBuilder.to_str(sort_hash)
|
|
41
|
+
scope.order(Arel.sql(sorting)) # rubocop:disable CodeReuse/ActiveRecord
|
|
27
42
|
end
|
|
28
43
|
|
|
29
44
|
protected
|
|
@@ -53,11 +68,13 @@ module Wallaby
|
|
|
53
68
|
valid_filter_name =
|
|
54
69
|
FilterUtils.filter_name_by(filter_name, @model_decorator.filters)
|
|
55
70
|
scope = find_scope(valid_filter_name)
|
|
56
|
-
if scope.blank?
|
|
57
|
-
|
|
71
|
+
return unscoped if scope.blank?
|
|
72
|
+
|
|
73
|
+
if scope.is_a?(Proc) then @model_class.instance_exec(&scope)
|
|
58
74
|
elsif @model_class.respond_to?(scope)
|
|
59
|
-
@model_class.
|
|
60
|
-
else
|
|
75
|
+
@model_class.try(scope)
|
|
76
|
+
else
|
|
77
|
+
unscoped
|
|
61
78
|
end
|
|
62
79
|
end
|
|
63
80
|
|
|
@@ -72,15 +89,28 @@ module Wallaby
|
|
|
72
89
|
|
|
73
90
|
# @return [ActiveRecord::Relation] Unscoped query
|
|
74
91
|
def unscoped
|
|
75
|
-
@model_class.where nil
|
|
92
|
+
@model_class.where nil # rubocop:disable CodeReuse/ActiveRecord
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Preload association for the scope
|
|
96
|
+
# @param scope [ActiveRecord::Relation]
|
|
97
|
+
# @return [ActiveRecord::Relation]
|
|
98
|
+
def preload_associations_for(scope)
|
|
99
|
+
index_associations = index_field_names.select do |field_name|
|
|
100
|
+
@model_decorator.fields.dig(field_name, :is_association)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
return scope if index_associations.blank?
|
|
104
|
+
|
|
105
|
+
scope.preload(*index_associations) # rubocop:disable CodeReuse/ActiveRecord
|
|
76
106
|
end
|
|
77
107
|
|
|
78
|
-
# Search text for the text columns (see {}) in
|
|
108
|
+
# Search text for the text columns (see {}) in {#index_field_names}
|
|
79
109
|
# @param keywords [String] keywords
|
|
80
|
-
# @param
|
|
81
|
-
# @return [
|
|
82
|
-
def text_search(keywords,
|
|
83
|
-
return
|
|
110
|
+
# @param arel_query [Arel::Nodes::Node, nil]
|
|
111
|
+
# @return [Arel::Nodes::Node, nil]
|
|
112
|
+
def text_search(keywords, arel_query = nil)
|
|
113
|
+
return arel_query unless keywords_check?(keywords)
|
|
84
114
|
|
|
85
115
|
text_fields.each do |field_name|
|
|
86
116
|
sub_query = nil
|
|
@@ -88,34 +118,31 @@ module Wallaby
|
|
|
88
118
|
exp = table[field_name].matches(Escaper.execute(keyword))
|
|
89
119
|
sub_query = sub_query.try(:and, exp) || exp
|
|
90
120
|
end
|
|
91
|
-
|
|
121
|
+
arel_query = arel_query.try(:or, sub_query) || sub_query
|
|
92
122
|
end
|
|
93
|
-
|
|
123
|
+
arel_query
|
|
94
124
|
end
|
|
95
125
|
|
|
96
126
|
# Perform SQL query for the colon query (e.g. data:<2000-01-01)
|
|
97
127
|
# @param field_queries [Array]
|
|
98
|
-
# @param
|
|
99
|
-
# @return [
|
|
100
|
-
def field_search(field_queries,
|
|
101
|
-
return
|
|
128
|
+
# @param arel_query [Arel::Nodes::Node]
|
|
129
|
+
# @return [Arel::Nodes::Node]
|
|
130
|
+
def field_search(field_queries, arel_query)
|
|
131
|
+
return arel_query unless field_check?(field_queries)
|
|
102
132
|
|
|
103
133
|
field_queries.each do |exps|
|
|
104
|
-
sub_queries = build_sub_queries_with
|
|
105
|
-
|
|
134
|
+
sub_queries = build_sub_queries_with(exps)
|
|
135
|
+
arel_query = arel_query.try(:and, sub_queries) || sub_queries
|
|
106
136
|
end
|
|
107
|
-
|
|
137
|
+
arel_query
|
|
108
138
|
end
|
|
109
139
|
|
|
110
|
-
# @return [Array<String>] a list of text fields from
|
|
140
|
+
# @return [Array<String>] a list of text fields from {#index_field_names}
|
|
111
141
|
def text_fields
|
|
112
|
-
@text_fields ||=
|
|
113
|
-
index_field_names
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
TEXT_FIELDS.include?(metadata[:type].to_s)
|
|
117
|
-
end.keys
|
|
118
|
-
end
|
|
142
|
+
@text_fields ||= @model_decorator.fields.select do |field_name, metadata|
|
|
143
|
+
index_field_names.include?(field_name) &&
|
|
144
|
+
TEXT_FIELDS.include?(metadata[:type].to_s)
|
|
145
|
+
end.keys
|
|
119
146
|
end
|
|
120
147
|
|
|
121
148
|
# @param keywords [Array<String>] a list of keywords
|
|
@@ -153,6 +180,30 @@ module Wallaby
|
|
|
153
180
|
end
|
|
154
181
|
query
|
|
155
182
|
end
|
|
183
|
+
|
|
184
|
+
def normalize_sort(hash)
|
|
185
|
+
sanitized =
|
|
186
|
+
hash.reject do |name, _sort|
|
|
187
|
+
@model_decorator.fields[name].blank? || # not a column or association?
|
|
188
|
+
@model_decorator.index_fields[name][:sort_disabled] || # sort disabled?
|
|
189
|
+
@model_decorator.index_field_names.exclude?(name) # not included?
|
|
190
|
+
end
|
|
191
|
+
sanitized.each_with_object({}) do |(field_name, value), normalized|
|
|
192
|
+
column_name = "#{@model_class.table_name}.#{field_name}"
|
|
193
|
+
normalized[column_name] =
|
|
194
|
+
case nulls_order = @model_decorator.index_fields.dig(field_name, :nulls)
|
|
195
|
+
when :first, :last, 'first', 'last'
|
|
196
|
+
"#{value} nulls #{nulls_order}"
|
|
197
|
+
else
|
|
198
|
+
value
|
|
199
|
+
end.upcase
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# @return [Array<String>]
|
|
204
|
+
def index_field_names
|
|
205
|
+
@index_field_names ||= @model_decorator.index_field_names.map(&:to_s)
|
|
206
|
+
end
|
|
156
207
|
end
|
|
157
208
|
end
|
|
158
209
|
end
|
|
@@ -30,7 +30,7 @@ module Wallaby
|
|
|
30
30
|
# @return [false] otherwise
|
|
31
31
|
def valid_range_type?(values, metadata)
|
|
32
32
|
!metadata \
|
|
33
|
-
||
|
|
33
|
+
|| %w[daterange tsrange tstzrange].exclude?(metadata[:type]) \
|
|
34
34
|
|| !values.try(:any?, &:blank?)
|
|
35
35
|
end
|
|
36
36
|
end
|
|
@@ -7,9 +7,9 @@ module Wallaby
|
|
|
7
7
|
# @param params [ActionController::Parameters]
|
|
8
8
|
# @param action [String, Symbol]
|
|
9
9
|
# @param authorizer
|
|
10
|
-
# @return [ActionController::Parameters]
|
|
10
|
+
# @return [ActionController::Parameters] allowlisted parameters
|
|
11
11
|
def permit(params, action, authorizer)
|
|
12
|
-
authorized_fields = authorizer.permit_params
|
|
12
|
+
authorized_fields = authorizer.permit_params(action, @model_class)
|
|
13
13
|
params.require(param_key).permit(authorized_fields || permitted_fields)
|
|
14
14
|
end
|
|
15
15
|
|
|
@@ -18,19 +18,18 @@ module Wallaby
|
|
|
18
18
|
# @param authorizer [Ability] for now
|
|
19
19
|
# @return [ActiveRecord::Relation] relation
|
|
20
20
|
def collection(params, authorizer)
|
|
21
|
-
query = querier.search
|
|
22
|
-
query =
|
|
23
|
-
authorizer.accessible_for
|
|
21
|
+
query = querier.search(params)
|
|
22
|
+
query = querier.sort(params[:sort], query)
|
|
23
|
+
authorizer.accessible_for(:index, query)
|
|
24
24
|
end
|
|
25
25
|
|
|
26
26
|
# @param query [ActiveRecord::Relation]
|
|
27
27
|
# @param params [ActionController::Parameters]
|
|
28
28
|
# @return [ActiveRecord::Relation] paginated query
|
|
29
29
|
def paginate(query, params)
|
|
30
|
-
per = (params[:per] || Wallaby.
|
|
30
|
+
per = (params[:per] || Wallaby.controller_configuration.try(:page_size)).to_i
|
|
31
31
|
page = [params[:page].to_i, 1].max # starting from page 1
|
|
32
|
-
query
|
|
33
|
-
query
|
|
32
|
+
query.offset((page - 1) * per).limit(per) # rubocop:disable CodeReuse/ActiveRecord
|
|
34
33
|
end
|
|
35
34
|
|
|
36
35
|
# @note No mass assignment happens here!
|
|
@@ -45,7 +44,7 @@ module Wallaby
|
|
|
45
44
|
# @return [Object] persisted resource object
|
|
46
45
|
# @raise [Wallaby::ResourceNotFound] when record is not found
|
|
47
46
|
def find(id, _params, _authorizer)
|
|
48
|
-
@model_class.find
|
|
47
|
+
@model_class.find(id)
|
|
49
48
|
rescue ::ActiveRecord::RecordNotFound
|
|
50
49
|
raise ResourceNotFound, id
|
|
51
50
|
end
|
|
@@ -55,7 +54,7 @@ module Wallaby
|
|
|
55
54
|
# @param params [ActionController::Parameters]
|
|
56
55
|
# @param authorizer [Wallaby::ModelAuthorizer]
|
|
57
56
|
def create(resource, params, authorizer)
|
|
58
|
-
save
|
|
57
|
+
save(__callee__, resource, params, authorizer)
|
|
59
58
|
end
|
|
60
59
|
|
|
61
60
|
# Assign resource with new values and store it in database as an update.
|
|
@@ -63,7 +62,7 @@ module Wallaby
|
|
|
63
62
|
# @param params [ActionController::Parameters]
|
|
64
63
|
# @param authorizer [Wallaby::ModelAuthorizer]
|
|
65
64
|
def update(resource, params, authorizer)
|
|
66
|
-
save
|
|
65
|
+
save(__callee__, resource, params, authorizer)
|
|
67
66
|
end
|
|
68
67
|
|
|
69
68
|
# Remove a record from database
|
|
@@ -82,19 +81,19 @@ module Wallaby
|
|
|
82
81
|
# @return resource itself
|
|
83
82
|
# @raise [ActiveRecord::StatementInvalid, ActiveModel::UnknownAttributeError, ActiveRecord::UnknownAttributeError]
|
|
84
83
|
def save(action, resource, params, authorizer)
|
|
85
|
-
resource.assign_attributes
|
|
86
|
-
ensure_attributes_for
|
|
87
|
-
resource.save if valid?
|
|
84
|
+
resource.assign_attributes(normalize(params))
|
|
85
|
+
ensure_attributes_for(authorizer, action, resource)
|
|
86
|
+
resource.save if valid?(resource)
|
|
88
87
|
resource
|
|
89
88
|
rescue ::ActiveRecord::ActiveRecordError, ActiveModel::ForbiddenAttributesError, unknown_attribute_error => e
|
|
90
|
-
resource.errors.add
|
|
89
|
+
resource.errors.add(:base, e.message)
|
|
91
90
|
resource
|
|
92
91
|
end
|
|
93
92
|
|
|
94
93
|
# Normalize params
|
|
95
94
|
# @param params [ActionController::Parameters]
|
|
96
95
|
def normalize(params)
|
|
97
|
-
normalizer.normalize
|
|
96
|
+
normalizer.normalize(params)
|
|
98
97
|
end
|
|
99
98
|
|
|
100
99
|
# See if a resource is valid
|
|
@@ -102,7 +101,7 @@ module Wallaby
|
|
|
102
101
|
# @return [true] if valid
|
|
103
102
|
# @return [false] otherwise
|
|
104
103
|
def valid?(resource)
|
|
105
|
-
validator.valid?
|
|
104
|
+
validator.valid?(resource)
|
|
106
105
|
end
|
|
107
106
|
|
|
108
107
|
# To make sure that the record can be updated with the values that are
|
|
@@ -113,8 +112,8 @@ module Wallaby
|
|
|
113
112
|
def ensure_attributes_for(authorizer, action, resource)
|
|
114
113
|
return if authorizer.blank?
|
|
115
114
|
|
|
116
|
-
restricted_conditions = authorizer.attributes_for
|
|
117
|
-
resource.assign_attributes
|
|
115
|
+
restricted_conditions = authorizer.attributes_for(action, resource)
|
|
116
|
+
resource.assign_attributes(restricted_conditions)
|
|
118
117
|
end
|
|
119
118
|
|
|
120
119
|
# @return [String] param key
|
|
@@ -122,7 +121,7 @@ module Wallaby
|
|
|
122
121
|
@model_class.model_name.param_key
|
|
123
122
|
end
|
|
124
123
|
|
|
125
|
-
# @return [Array] the list of attributes to
|
|
124
|
+
# @return [Array] the list of attributes to allowlist for mass assignment
|
|
126
125
|
def permitted_fields
|
|
127
126
|
@permitted_fields ||=
|
|
128
127
|
permitter.simple_field_names << permitter.compound_hashed_fields
|
|
@@ -130,22 +129,22 @@ module Wallaby
|
|
|
130
129
|
|
|
131
130
|
# @return [Wallaby::ActiveRecord::ModelServiceProvider::Permitter]
|
|
132
131
|
def permitter
|
|
133
|
-
@permitter ||= Permitter.new
|
|
132
|
+
@permitter ||= Permitter.new(@model_decorator)
|
|
134
133
|
end
|
|
135
134
|
|
|
136
135
|
# @return [Wallaby::ActiveRecord::ModelServiceProvider::Querier]
|
|
137
136
|
def querier
|
|
138
|
-
@querier ||= Querier.new
|
|
137
|
+
@querier ||= Querier.new(@model_decorator)
|
|
139
138
|
end
|
|
140
139
|
|
|
141
140
|
# @return [Wallaby::ActiveRecord::ModelServiceProvider::Normalizer]
|
|
142
141
|
def normalizer
|
|
143
|
-
@normalizer ||= Normalizer.new
|
|
142
|
+
@normalizer ||= Normalizer.new(@model_decorator)
|
|
144
143
|
end
|
|
145
144
|
|
|
146
145
|
# @return [Wallaby::ActiveRecord::ModelServiceProvider::Validator]
|
|
147
146
|
def validator
|
|
148
|
-
@validator ||= Validator.new
|
|
147
|
+
@validator ||= Validator.new(@model_decorator)
|
|
149
148
|
end
|
|
150
149
|
|
|
151
150
|
# @return [Class] ActiveModel::UnknownAttributeError if Rails 4
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: wallaby-active_record
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0.beta1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Tian Chen
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date:
|
|
11
|
+
date: 2023-09-22 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: activerecord
|
|
@@ -28,16 +28,16 @@ dependencies:
|
|
|
28
28
|
name: wallaby-core
|
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
|
30
30
|
requirements:
|
|
31
|
-
- -
|
|
31
|
+
- - '='
|
|
32
32
|
- !ruby/object:Gem::Version
|
|
33
|
-
version: 0.
|
|
33
|
+
version: 0.3.0.beta1
|
|
34
34
|
type: :runtime
|
|
35
35
|
prerelease: false
|
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
|
37
37
|
requirements:
|
|
38
|
-
- -
|
|
38
|
+
- - '='
|
|
39
39
|
- !ruby/object:Gem::Version
|
|
40
|
-
version: 0.
|
|
40
|
+
version: 0.3.0.beta1
|
|
41
41
|
- !ruby/object:Gem::Dependency
|
|
42
42
|
name: cancancan
|
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -132,6 +132,7 @@ metadata:
|
|
|
132
132
|
homepage_uri: https://github.com/wallaby-rails/wallaby-active_record
|
|
133
133
|
source_code_uri: https://github.com/wallaby-rails/wallaby-active_record
|
|
134
134
|
changelog_uri: https://github.com/wallaby-rails/wallaby-active_record/blob/master/CHANGELOG.md
|
|
135
|
+
rubygems_mfa_required: 'true'
|
|
135
136
|
post_install_message:
|
|
136
137
|
rdoc_options: []
|
|
137
138
|
require_paths:
|
|
@@ -143,11 +144,11 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
143
144
|
version: '0'
|
|
144
145
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
145
146
|
requirements:
|
|
146
|
-
- - "
|
|
147
|
+
- - ">"
|
|
147
148
|
- !ruby/object:Gem::Version
|
|
148
|
-
version:
|
|
149
|
+
version: 1.3.1
|
|
149
150
|
requirements: []
|
|
150
|
-
rubygems_version: 3.
|
|
151
|
+
rubygems_version: 3.3.7
|
|
151
152
|
signing_key:
|
|
152
153
|
specification_version: 4
|
|
153
154
|
summary: Wallaby's ActiveRecord ORM adapter
|