apiwork 0.0.0.pre → 0.1.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 +4 -4
- data/LICENSE.txt +2 -2
- data/README.md +117 -1
- data/Rakefile +5 -3
- data/app/controllers/apiwork/errors_controller.rb +13 -0
- data/app/controllers/apiwork/exports_controller.rb +22 -0
- data/lib/apiwork/abstractable.rb +26 -0
- data/lib/apiwork/adapter/base.rb +369 -0
- data/lib/apiwork/adapter/builder/api/base.rb +66 -0
- data/lib/apiwork/adapter/builder/contract/base.rb +86 -0
- data/lib/apiwork/adapter/capability/api/base.rb +51 -0
- data/lib/apiwork/adapter/capability/api/scope.rb +64 -0
- data/lib/apiwork/adapter/capability/base.rb +291 -0
- data/lib/apiwork/adapter/capability/contract/base.rb +37 -0
- data/lib/apiwork/adapter/capability/contract/scope.rb +110 -0
- data/lib/apiwork/adapter/capability/operation/base.rb +172 -0
- data/lib/apiwork/adapter/capability/operation/metadata_shape.rb +165 -0
- data/lib/apiwork/adapter/capability/result.rb +21 -0
- data/lib/apiwork/adapter/capability/runner.rb +56 -0
- data/lib/apiwork/adapter/capability/transformer/request/base.rb +72 -0
- data/lib/apiwork/adapter/capability/transformer/response/base.rb +45 -0
- data/lib/apiwork/adapter/registry.rb +16 -0
- data/lib/apiwork/adapter/serializer/error/base.rb +72 -0
- data/lib/apiwork/adapter/serializer/error/default/api_builder.rb +32 -0
- data/lib/apiwork/adapter/serializer/error/default.rb +37 -0
- data/lib/apiwork/adapter/serializer/resource/base.rb +84 -0
- data/lib/apiwork/adapter/serializer/resource/default/contract_builder.rb +209 -0
- data/lib/apiwork/adapter/serializer/resource/default.rb +39 -0
- data/lib/apiwork/adapter/standard/capability/filtering/api_builder.rb +75 -0
- data/lib/apiwork/adapter/standard/capability/filtering/constants.rb +37 -0
- data/lib/apiwork/adapter/standard/capability/filtering/contract_builder.rb +193 -0
- data/lib/apiwork/adapter/standard/capability/filtering/operation/filter/builder.rb +47 -0
- data/lib/apiwork/adapter/standard/capability/filtering/operation/filter/operator_builder.rb +36 -0
- data/lib/apiwork/adapter/standard/capability/filtering/operation/filter.rb +462 -0
- data/lib/apiwork/adapter/standard/capability/filtering/operation.rb +22 -0
- data/lib/apiwork/adapter/standard/capability/filtering/request_transformer.rb +47 -0
- data/lib/apiwork/adapter/standard/capability/filtering.rb +18 -0
- data/lib/apiwork/adapter/standard/capability/including/contract_builder.rb +169 -0
- data/lib/apiwork/adapter/standard/capability/including/operation.rb +20 -0
- data/lib/apiwork/adapter/standard/capability/including.rb +16 -0
- data/lib/apiwork/adapter/standard/capability/pagination/api_builder.rb +34 -0
- data/lib/apiwork/adapter/standard/capability/pagination/contract_builder.rb +35 -0
- data/lib/apiwork/adapter/standard/capability/pagination/operation/paginate/cursor.rb +84 -0
- data/lib/apiwork/adapter/standard/capability/pagination/operation/paginate/offset.rb +66 -0
- data/lib/apiwork/adapter/standard/capability/pagination/operation/paginate.rb +24 -0
- data/lib/apiwork/adapter/standard/capability/pagination/operation.rb +24 -0
- data/lib/apiwork/adapter/standard/capability/pagination.rb +21 -0
- data/lib/apiwork/adapter/standard/capability/sorting/api_builder.rb +19 -0
- data/lib/apiwork/adapter/standard/capability/sorting/contract_builder.rb +84 -0
- data/lib/apiwork/adapter/standard/capability/sorting/operation/sort.rb +83 -0
- data/lib/apiwork/adapter/standard/capability/sorting/operation.rb +22 -0
- data/lib/apiwork/adapter/standard/capability/sorting.rb +17 -0
- data/lib/apiwork/adapter/standard/capability/writing/constants.rb +15 -0
- data/lib/apiwork/adapter/standard/capability/writing/contract_builder.rb +253 -0
- data/lib/apiwork/adapter/standard/capability/writing/operation/issue_mapper.rb +210 -0
- data/lib/apiwork/adapter/standard/capability/writing/operation.rb +32 -0
- data/lib/apiwork/adapter/standard/capability/writing/request_transformer.rb +37 -0
- data/lib/apiwork/adapter/standard/capability/writing.rb +17 -0
- data/lib/apiwork/adapter/standard/includes_resolver.rb +106 -0
- data/lib/apiwork/adapter/standard.rb +22 -0
- data/lib/apiwork/adapter/wrapper/base.rb +70 -0
- data/lib/apiwork/adapter/wrapper/collection/base.rb +60 -0
- data/lib/apiwork/adapter/wrapper/collection/default.rb +47 -0
- data/lib/apiwork/adapter/wrapper/error/base.rb +30 -0
- data/lib/apiwork/adapter/wrapper/error/default.rb +34 -0
- data/lib/apiwork/adapter/wrapper/member/base.rb +58 -0
- data/lib/apiwork/adapter/wrapper/member/default.rb +40 -0
- data/lib/apiwork/adapter/wrapper/shape.rb +203 -0
- data/lib/apiwork/adapter.rb +50 -0
- data/lib/apiwork/api/base.rb +802 -0
- data/lib/apiwork/api/element.rb +110 -0
- data/lib/apiwork/api/enum_registry/definition.rb +51 -0
- data/lib/apiwork/api/enum_registry.rb +98 -0
- data/lib/apiwork/api/info/contact.rb +67 -0
- data/lib/apiwork/api/info/license.rb +50 -0
- data/lib/apiwork/api/info/server.rb +50 -0
- data/lib/apiwork/api/info.rb +221 -0
- data/lib/apiwork/api/object.rb +235 -0
- data/lib/apiwork/api/registry.rb +33 -0
- data/lib/apiwork/api/representation_registry.rb +76 -0
- data/lib/apiwork/api/resource/action.rb +41 -0
- data/lib/apiwork/api/resource.rb +648 -0
- data/lib/apiwork/api/router.rb +104 -0
- data/lib/apiwork/api/type_registry/definition.rb +117 -0
- data/lib/apiwork/api/type_registry.rb +99 -0
- data/lib/apiwork/api/union.rb +49 -0
- data/lib/apiwork/api.rb +85 -0
- data/lib/apiwork/configurable.rb +71 -0
- data/lib/apiwork/configuration/option.rb +125 -0
- data/lib/apiwork/configuration/validatable.rb +25 -0
- data/lib/apiwork/configuration.rb +95 -0
- data/lib/apiwork/configuration_error.rb +6 -0
- data/lib/apiwork/constraint_error.rb +20 -0
- data/lib/apiwork/contract/action/request.rb +79 -0
- data/lib/apiwork/contract/action/response.rb +87 -0
- data/lib/apiwork/contract/action.rb +258 -0
- data/lib/apiwork/contract/base.rb +714 -0
- data/lib/apiwork/contract/element.rb +130 -0
- data/lib/apiwork/contract/object/coercer.rb +194 -0
- data/lib/apiwork/contract/object/deserializer.rb +101 -0
- data/lib/apiwork/contract/object/transformer.rb +95 -0
- data/lib/apiwork/contract/object/validator/result.rb +27 -0
- data/lib/apiwork/contract/object/validator.rb +734 -0
- data/lib/apiwork/contract/object.rb +566 -0
- data/lib/apiwork/contract/request_parser/result.rb +25 -0
- data/lib/apiwork/contract/request_parser.rb +72 -0
- data/lib/apiwork/contract/response_parser/result.rb +25 -0
- data/lib/apiwork/contract/response_parser.rb +35 -0
- data/lib/apiwork/contract/union.rb +56 -0
- data/lib/apiwork/contract_error.rb +9 -0
- data/lib/apiwork/controller.rb +300 -0
- data/lib/apiwork/domain_error.rb +13 -0
- data/lib/apiwork/element.rb +386 -0
- data/lib/apiwork/engine.rb +20 -0
- data/lib/apiwork/error.rb +6 -0
- data/lib/apiwork/error_code/definition.rb +63 -0
- data/lib/apiwork/error_code/registry.rb +18 -0
- data/lib/apiwork/error_code.rb +132 -0
- data/lib/apiwork/export/base.rb +291 -0
- data/lib/apiwork/export/open_api.rb +600 -0
- data/lib/apiwork/export/pipeline/writer.rb +66 -0
- data/lib/apiwork/export/pipeline.rb +84 -0
- data/lib/apiwork/export/registry.rb +16 -0
- data/lib/apiwork/export/surface_resolver.rb +189 -0
- data/lib/apiwork/export/type_analysis.rb +170 -0
- data/lib/apiwork/export/type_script.rb +23 -0
- data/lib/apiwork/export/type_script_mapper.rb +349 -0
- data/lib/apiwork/export/zod.rb +39 -0
- data/lib/apiwork/export/zod_mapper.rb +421 -0
- data/lib/apiwork/export.rb +80 -0
- data/lib/apiwork/http_error.rb +16 -0
- data/lib/apiwork/introspection/action/request.rb +66 -0
- data/lib/apiwork/introspection/action/response.rb +57 -0
- data/lib/apiwork/introspection/action.rb +124 -0
- data/lib/apiwork/introspection/api/info/contact.rb +59 -0
- data/lib/apiwork/introspection/api/info/license.rb +49 -0
- data/lib/apiwork/introspection/api/info/server.rb +50 -0
- data/lib/apiwork/introspection/api/info.rb +107 -0
- data/lib/apiwork/introspection/api/resource.rb +83 -0
- data/lib/apiwork/introspection/api.rb +92 -0
- data/lib/apiwork/introspection/contract.rb +63 -0
- data/lib/apiwork/introspection/dump/action.rb +101 -0
- data/lib/apiwork/introspection/dump/api.rb +119 -0
- data/lib/apiwork/introspection/dump/contract.rb +129 -0
- data/lib/apiwork/introspection/dump/param.rb +486 -0
- data/lib/apiwork/introspection/dump/resource.rb +112 -0
- data/lib/apiwork/introspection/dump/type.rb +339 -0
- data/lib/apiwork/introspection/dump.rb +17 -0
- data/lib/apiwork/introspection/enum.rb +63 -0
- data/lib/apiwork/introspection/error_code.rb +44 -0
- data/lib/apiwork/introspection/param/array.rb +88 -0
- data/lib/apiwork/introspection/param/base.rb +285 -0
- data/lib/apiwork/introspection/param/binary.rb +73 -0
- data/lib/apiwork/introspection/param/boolean.rb +73 -0
- data/lib/apiwork/introspection/param/date.rb +73 -0
- data/lib/apiwork/introspection/param/date_time.rb +73 -0
- data/lib/apiwork/introspection/param/decimal.rb +121 -0
- data/lib/apiwork/introspection/param/integer.rb +131 -0
- data/lib/apiwork/introspection/param/literal.rb +45 -0
- data/lib/apiwork/introspection/param/number.rb +121 -0
- data/lib/apiwork/introspection/param/object.rb +59 -0
- data/lib/apiwork/introspection/param/reference.rb +45 -0
- data/lib/apiwork/introspection/param/string.rb +122 -0
- data/lib/apiwork/introspection/param/time.rb +73 -0
- data/lib/apiwork/introspection/param/union.rb +57 -0
- data/lib/apiwork/introspection/param/unknown.rb +26 -0
- data/lib/apiwork/introspection/param/uuid.rb +73 -0
- data/lib/apiwork/introspection/param.rb +31 -0
- data/lib/apiwork/introspection/type.rb +129 -0
- data/lib/apiwork/introspection.rb +28 -0
- data/lib/apiwork/issue.rb +80 -0
- data/lib/apiwork/json_pointer.rb +21 -0
- data/lib/apiwork/object.rb +1618 -0
- data/lib/apiwork/reference_generator.rb +622 -0
- data/lib/apiwork/registry.rb +56 -0
- data/lib/apiwork/representation/association.rb +391 -0
- data/lib/apiwork/representation/attribute.rb +335 -0
- data/lib/apiwork/representation/base.rb +819 -0
- data/lib/apiwork/representation/deserializer.rb +95 -0
- data/lib/apiwork/representation/element.rb +128 -0
- data/lib/apiwork/representation/inheritance.rb +78 -0
- data/lib/apiwork/representation/model_detector.rb +75 -0
- data/lib/apiwork/representation/root_key.rb +35 -0
- data/lib/apiwork/representation/serializer.rb +127 -0
- data/lib/apiwork/request.rb +79 -0
- data/lib/apiwork/response.rb +56 -0
- data/lib/apiwork/union.rb +102 -0
- data/lib/apiwork/version.rb +2 -2
- data/lib/apiwork.rb +61 -3
- data/lib/generators/apiwork/api_generator.rb +38 -0
- data/lib/generators/apiwork/contract_generator.rb +25 -0
- data/lib/generators/apiwork/install_generator.rb +27 -0
- data/lib/generators/apiwork/representation_generator.rb +25 -0
- data/lib/generators/apiwork/templates/api/api.rb.tt +4 -0
- data/lib/generators/apiwork/templates/contract/contract.rb.tt +6 -0
- data/lib/generators/apiwork/templates/install/application_contract.rb.tt +5 -0
- data/lib/generators/apiwork/templates/install/application_representation.rb.tt +5 -0
- data/lib/generators/apiwork/templates/representation/representation.rb.tt +6 -0
- data/lib/tasks/apiwork.rake +102 -0
- metadata +319 -19
- data/.rubocop.yml +0 -8
- data/sig/apiwork.rbs +0 -4
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Apiwork
|
|
4
|
+
module Adapter
|
|
5
|
+
class Standard
|
|
6
|
+
module Capability
|
|
7
|
+
class Filtering
|
|
8
|
+
class Operation
|
|
9
|
+
class Filter
|
|
10
|
+
class OperatorBuilder
|
|
11
|
+
attr_reader :column,
|
|
12
|
+
:field_name,
|
|
13
|
+
:valid_operators
|
|
14
|
+
|
|
15
|
+
def initialize(column, field_name, valid_operators:)
|
|
16
|
+
@column = column
|
|
17
|
+
@field_name = field_name
|
|
18
|
+
@valid_operators = valid_operators
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def build(operator_hash)
|
|
22
|
+
operator_hash.filter_map do |operator, compare_value|
|
|
23
|
+
operator = operator.to_sym
|
|
24
|
+
next unless valid_operators.include?(operator)
|
|
25
|
+
|
|
26
|
+
yield(operator, compare_value)
|
|
27
|
+
end.reduce(:and)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Apiwork
|
|
4
|
+
module Adapter
|
|
5
|
+
class Standard
|
|
6
|
+
module Capability
|
|
7
|
+
class Filtering
|
|
8
|
+
class Operation
|
|
9
|
+
class Filter
|
|
10
|
+
attr_reader :representation_class
|
|
11
|
+
|
|
12
|
+
class << self
|
|
13
|
+
def apply(relation, representation_class, params)
|
|
14
|
+
new(relation, representation_class).apply(params)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def initialize(relation, representation_class)
|
|
19
|
+
@relation = relation
|
|
20
|
+
@representation_class = representation_class
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def apply(params)
|
|
24
|
+
data = filter_data(params)
|
|
25
|
+
includes = IncludesResolver.resolve(representation_class, params)
|
|
26
|
+
{ data:, includes: }
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def build_where_conditions(filter, target_klass = representation_class.model_class)
|
|
30
|
+
filter.each_with_object([[], {}]) do |(key, value), (conditions, joins)|
|
|
31
|
+
key = key.to_sym
|
|
32
|
+
|
|
33
|
+
if (attribute = representation_class.attributes[key])&.filterable?
|
|
34
|
+
next unless filterable_for_context?(attribute)
|
|
35
|
+
|
|
36
|
+
if (condition = build_column_condition(key, value, target_klass))
|
|
37
|
+
conditions << condition
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
elsif (association = find_filterable_association(key))
|
|
41
|
+
association_conditions, association_joins = build_join_conditions(key, value, association)
|
|
42
|
+
conditions.concat(association_conditions)
|
|
43
|
+
joins.deep_merge!(association_joins)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def filter_data(params)
|
|
51
|
+
return @relation if params.blank?
|
|
52
|
+
|
|
53
|
+
case params
|
|
54
|
+
when Hash
|
|
55
|
+
apply_hash_filter(params)
|
|
56
|
+
when Array
|
|
57
|
+
apply_array_filter(params)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def apply_hash_filter(params)
|
|
62
|
+
logical_operators, regular_attributes = separate_logical_operators(params)
|
|
63
|
+
|
|
64
|
+
scope = @relation
|
|
65
|
+
|
|
66
|
+
if regular_attributes.present?
|
|
67
|
+
conditions, joins = build_where_conditions(regular_attributes, representation_class.model_class)
|
|
68
|
+
scope = with_joins_and_distinct(scope, joins) { |scoped| scoped.where(conditions.reduce(:and)) } if conditions.any?
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
scope = apply_not(scope, logical_operators[Constants::NOT]) if logical_operators.key?(Constants::NOT)
|
|
72
|
+
scope = apply_or(scope, logical_operators[Constants::OR]) if logical_operators.key?(Constants::OR)
|
|
73
|
+
scope = apply_and(scope, logical_operators[Constants::AND]) if logical_operators.key?(Constants::AND)
|
|
74
|
+
|
|
75
|
+
scope
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def apply_array_filter(params)
|
|
79
|
+
return @relation if params.empty?
|
|
80
|
+
|
|
81
|
+
individual_conditions = params.filter_map do |filter_hash|
|
|
82
|
+
conditions, _joins = build_where_conditions(filter_hash, representation_class.model_class)
|
|
83
|
+
conditions.compact.reduce(:and) if conditions.any?
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
all_joins = params
|
|
87
|
+
.map { |filter_params| build_where_conditions(filter_params, representation_class.model_class)[1] }
|
|
88
|
+
.each_with_object({}) { |joins, accumulated| accumulated.deep_merge!(joins) }
|
|
89
|
+
|
|
90
|
+
with_joins_and_distinct(@relation, all_joins) do |scope|
|
|
91
|
+
if individual_conditions.any?
|
|
92
|
+
scope.where(individual_conditions.reduce(:or))
|
|
93
|
+
else
|
|
94
|
+
scope
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def apply_not(scope, filter_params)
|
|
100
|
+
condition, joins = build_conditions_recursive(filter_params)
|
|
101
|
+
return scope if condition.nil?
|
|
102
|
+
|
|
103
|
+
with_joins_and_distinct(scope, joins) { |scoped| scoped.where.not(condition) }
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def apply_or(scope, conditions_array)
|
|
107
|
+
return scope if conditions_array.blank?
|
|
108
|
+
|
|
109
|
+
or_conditions = []
|
|
110
|
+
all_joins = {}
|
|
111
|
+
|
|
112
|
+
conditions_array.each do |filter_hash|
|
|
113
|
+
conditions, joins = build_conditions_recursive(filter_hash)
|
|
114
|
+
or_conditions << conditions if conditions
|
|
115
|
+
all_joins = all_joins.deep_merge(joins)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
with_joins_and_distinct(scope, all_joins) do |scoped|
|
|
119
|
+
if or_conditions.any?
|
|
120
|
+
scoped.where(or_conditions.compact.reduce(:or))
|
|
121
|
+
else
|
|
122
|
+
scoped
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def apply_and(scope, conditions_array)
|
|
128
|
+
return scope if conditions_array.blank?
|
|
129
|
+
|
|
130
|
+
conditions_array.reduce(scope) do |current_scope, filter_hash|
|
|
131
|
+
Filter.apply(current_scope, representation_class, filter_hash)[:data]
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def build_conditions_recursive(filter_params)
|
|
136
|
+
return [nil, {}] if filter_params.blank?
|
|
137
|
+
return [nil, {}] unless filter_params.is_a?(Hash)
|
|
138
|
+
|
|
139
|
+
logical_operators, regular_attributes = separate_logical_operators(filter_params)
|
|
140
|
+
|
|
141
|
+
conditions = []
|
|
142
|
+
all_joins = {}
|
|
143
|
+
|
|
144
|
+
if regular_attributes.present?
|
|
145
|
+
attribute_conditions, joins = build_where_conditions(regular_attributes, representation_class.model_class)
|
|
146
|
+
conditions << attribute_conditions.reduce(:and) if attribute_conditions.any?
|
|
147
|
+
all_joins = all_joins.deep_merge(joins)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
if logical_operators.key?(Constants::AND)
|
|
151
|
+
condition, joins = process_logical_operator(logical_operators[Constants::AND], :and)
|
|
152
|
+
conditions << condition if condition
|
|
153
|
+
all_joins = all_joins.deep_merge(joins)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
if logical_operators.key?(Constants::OR)
|
|
157
|
+
condition, joins = process_logical_operator(logical_operators[Constants::OR], :or)
|
|
158
|
+
conditions << condition if condition
|
|
159
|
+
all_joins = all_joins.deep_merge(joins)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
if logical_operators.key?(Constants::NOT)
|
|
163
|
+
not_condition, joins = build_conditions_recursive(logical_operators[Constants::NOT])
|
|
164
|
+
conditions << not_condition.not if not_condition
|
|
165
|
+
all_joins = all_joins.deep_merge(joins)
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
[conditions.compact.reduce(:and), all_joins]
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def process_logical_operator(filters, combinator)
|
|
172
|
+
collected_conditions = []
|
|
173
|
+
all_joins = {}
|
|
174
|
+
|
|
175
|
+
filters.each do |filter_hash|
|
|
176
|
+
condition, joins = build_conditions_recursive(filter_hash)
|
|
177
|
+
collected_conditions << condition if condition
|
|
178
|
+
all_joins = all_joins.deep_merge(joins)
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
[collected_conditions.any? ? collected_conditions.reduce(combinator) : nil, all_joins]
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def filterable_for_context?(attribute)
|
|
185
|
+
return true unless attribute.filterable?.is_a?(Proc)
|
|
186
|
+
|
|
187
|
+
representation_class.new(nil, {}).instance_eval(&attribute.filterable?)
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def build_column_condition(key, value, target_klass)
|
|
191
|
+
association = representation_class.polymorphic_association_for_type_column(key)
|
|
192
|
+
value = transform_polymorphic_filter_value(value, association) if association
|
|
193
|
+
|
|
194
|
+
inheritance = representation_class.inheritance_for_column(key)
|
|
195
|
+
value = transform_sti_filter_value(value, inheritance) if inheritance
|
|
196
|
+
|
|
197
|
+
column_type = target_klass.type_for_attribute(key).type
|
|
198
|
+
return nil if column_type.nil?
|
|
199
|
+
|
|
200
|
+
case column_type
|
|
201
|
+
when :uuid
|
|
202
|
+
build_uuid_where_clause(key, value, target_klass)
|
|
203
|
+
when :string, :text, :binary
|
|
204
|
+
build_string_where_clause(key, value, target_klass)
|
|
205
|
+
when :date, :datetime
|
|
206
|
+
build_date_where_clause(key, value, target_klass)
|
|
207
|
+
when :time
|
|
208
|
+
build_time_where_clause(key, value, target_klass)
|
|
209
|
+
when :decimal, :integer, :float
|
|
210
|
+
build_numeric_where_clause(key, value, target_klass)
|
|
211
|
+
when :boolean
|
|
212
|
+
build_boolean_where_clause(key, value, target_klass)
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def transform_polymorphic_filter_value(value, association)
|
|
217
|
+
mapping = build_polymorphic_type_mapping(association)
|
|
218
|
+
|
|
219
|
+
case value
|
|
220
|
+
when String
|
|
221
|
+
mapping[value] || value
|
|
222
|
+
when Hash
|
|
223
|
+
value.transform_values { |item| transform_polymorphic_filter_value(item, association) }
|
|
224
|
+
when Array
|
|
225
|
+
value.map { |item| transform_polymorphic_filter_value(item, association) }
|
|
226
|
+
else
|
|
227
|
+
value
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def build_polymorphic_type_mapping(association)
|
|
232
|
+
association.polymorphic.each_with_object({}) do |representation_class, mapping|
|
|
233
|
+
mapping[representation_class.polymorphic_name] = representation_class.model_class.polymorphic_name
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def transform_sti_filter_value(value, inheritance)
|
|
238
|
+
mapping = inheritance.mapping
|
|
239
|
+
|
|
240
|
+
case value
|
|
241
|
+
when String
|
|
242
|
+
mapping[value] || value
|
|
243
|
+
when Hash
|
|
244
|
+
value.transform_values { |item| transform_sti_filter_value(item, inheritance) }
|
|
245
|
+
when Array
|
|
246
|
+
value.map { |item| transform_sti_filter_value(item, inheritance) }
|
|
247
|
+
else
|
|
248
|
+
value
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def find_filterable_association(key)
|
|
253
|
+
association = representation_class.associations[key]
|
|
254
|
+
return unless association
|
|
255
|
+
return unless association.filterable?
|
|
256
|
+
|
|
257
|
+
association
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
def build_join_conditions(key, value, association)
|
|
261
|
+
reflection = representation_class.model_class.reflect_on_association(key)
|
|
262
|
+
return [[], {}] unless reflection
|
|
263
|
+
return [[], {}] unless association.representation_class
|
|
264
|
+
|
|
265
|
+
nested_query = Filter.new(reflection.klass.all, association.representation_class)
|
|
266
|
+
nested_conditions, nested_joins = nested_query.build_where_conditions(value, reflection.klass)
|
|
267
|
+
|
|
268
|
+
[nested_conditions, { key => (nested_joins.any? ? nested_joins : {}) }]
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
def build_uuid_where_clause(key, value, target_klass)
|
|
272
|
+
column = target_klass.arel_table[key]
|
|
273
|
+
|
|
274
|
+
normalizer = lambda do |value|
|
|
275
|
+
case value
|
|
276
|
+
when String
|
|
277
|
+
value.include?(',') ? { in: value.split(',') } : { eq: value }
|
|
278
|
+
when Array
|
|
279
|
+
{ in: value }
|
|
280
|
+
else
|
|
281
|
+
value
|
|
282
|
+
end
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
builder = Builder.new(column, key, allowed_types: [Hash])
|
|
286
|
+
|
|
287
|
+
builder.build(value, normalizer:, valid_operators: Constants::NULLABLE_UUID_OPERATORS) do |operator, compare|
|
|
288
|
+
case operator
|
|
289
|
+
when :eq then column.eq(compare)
|
|
290
|
+
when :in then column.in(compare)
|
|
291
|
+
when :null then handle_null_operator(column, compare)
|
|
292
|
+
end
|
|
293
|
+
end
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
def build_string_where_clause(key, value, target_klass)
|
|
297
|
+
column = target_klass.arel_table[key]
|
|
298
|
+
|
|
299
|
+
normalizer = ->(value) { value.is_a?(String) || value.nil? ? { eq: value } : value }
|
|
300
|
+
|
|
301
|
+
builder = Builder.new(column, key, allowed_types: [Hash])
|
|
302
|
+
|
|
303
|
+
builder.build(
|
|
304
|
+
value,
|
|
305
|
+
normalizer:,
|
|
306
|
+
valid_operators: Constants::NULLABLE_STRING_OPERATORS,
|
|
307
|
+
) do |operator, compare|
|
|
308
|
+
case operator
|
|
309
|
+
when :eq then column.eq(compare)
|
|
310
|
+
when :contains then case_sensitive_pattern_match(column, "%#{compare}%")
|
|
311
|
+
when :starts_with then case_sensitive_pattern_match(column, "#{compare}%")
|
|
312
|
+
when :ends_with then case_sensitive_pattern_match(column, "%#{compare}")
|
|
313
|
+
when :in then column.in(compare)
|
|
314
|
+
when :null then handle_null_operator(column, compare)
|
|
315
|
+
end
|
|
316
|
+
end
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
def build_date_where_clause(key, value, target_klass)
|
|
320
|
+
column = target_klass.arel_table[key]
|
|
321
|
+
|
|
322
|
+
return handle_nil_value(column) if value.nil?
|
|
323
|
+
return column.eq(value) unless value.is_a?(Hash)
|
|
324
|
+
|
|
325
|
+
normalizer = ->(value) { value }
|
|
326
|
+
|
|
327
|
+
builder = Builder.new(column, key, allowed_types: [Hash])
|
|
328
|
+
|
|
329
|
+
builder.build(value, normalizer:, valid_operators: Constants::NULLABLE_DATE_OPERATORS) do |operator, compare|
|
|
330
|
+
case operator
|
|
331
|
+
when :null then handle_null_operator(column, compare)
|
|
332
|
+
when :between
|
|
333
|
+
from_date = compare[:from]
|
|
334
|
+
to_date = compare[:to]
|
|
335
|
+
next unless from_date && to_date
|
|
336
|
+
|
|
337
|
+
column.gteq(from_date.beginning_of_day).and(column.lteq(to_date.end_of_day))
|
|
338
|
+
when :eq then column.eq(compare)
|
|
339
|
+
when :gt then column.gt(compare)
|
|
340
|
+
when :gte then column.gteq(compare)
|
|
341
|
+
when :lt then column.lt(compare)
|
|
342
|
+
when :lte then column.lteq(compare)
|
|
343
|
+
when :in then column.in(Array(compare))
|
|
344
|
+
end
|
|
345
|
+
end
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
def handle_nil_value(column)
|
|
349
|
+
column.eq(nil)
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
def build_time_where_clause(key, value, target_klass)
|
|
353
|
+
column = target_klass.arel_table[key]
|
|
354
|
+
|
|
355
|
+
return handle_nil_value(column) if value.nil?
|
|
356
|
+
return column.eq(value) unless value.is_a?(Hash)
|
|
357
|
+
|
|
358
|
+
normalizer = ->(value) { value }
|
|
359
|
+
|
|
360
|
+
builder = Builder.new(column, key, allowed_types: [Hash])
|
|
361
|
+
|
|
362
|
+
builder.build(value, normalizer:, valid_operators: Constants::NULLABLE_DATE_OPERATORS) do |operator, compare|
|
|
363
|
+
case operator
|
|
364
|
+
when :null then handle_null_operator(column, compare)
|
|
365
|
+
when :between
|
|
366
|
+
from_time = compare[:from]
|
|
367
|
+
to_time = compare[:to]
|
|
368
|
+
next unless from_time && to_time
|
|
369
|
+
|
|
370
|
+
column.gteq(from_time).and(column.lteq(to_time))
|
|
371
|
+
when :eq then column.eq(compare)
|
|
372
|
+
when :gt then column.gt(compare)
|
|
373
|
+
when :gte then column.gteq(compare)
|
|
374
|
+
when :lt then column.lt(compare)
|
|
375
|
+
when :lte then column.lteq(compare)
|
|
376
|
+
when :in then column.in(Array(compare))
|
|
377
|
+
end
|
|
378
|
+
end
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
def build_numeric_where_clause(key, value, target_klass)
|
|
382
|
+
column = target_klass.arel_table[key]
|
|
383
|
+
|
|
384
|
+
normalizer = ->(value) { value.is_a?(Numeric) || value.nil? ? { eq: value } : value }
|
|
385
|
+
|
|
386
|
+
builder = Builder.new(column, key, allowed_types: [Hash])
|
|
387
|
+
|
|
388
|
+
builder.build(
|
|
389
|
+
value,
|
|
390
|
+
normalizer:,
|
|
391
|
+
valid_operators: Constants::NULLABLE_NUMERIC_OPERATORS,
|
|
392
|
+
) do |operator, compare|
|
|
393
|
+
case operator
|
|
394
|
+
when :eq then column.eq(compare)
|
|
395
|
+
when :gt then column.gt(compare)
|
|
396
|
+
when :gte then column.gteq(compare)
|
|
397
|
+
when :lt then column.lt(compare)
|
|
398
|
+
when :lte then column.lteq(compare)
|
|
399
|
+
when :between
|
|
400
|
+
from_number = compare[:from]
|
|
401
|
+
to_number = compare[:to]
|
|
402
|
+
next unless from_number && to_number
|
|
403
|
+
|
|
404
|
+
column.between(from_number..to_number)
|
|
405
|
+
when :in
|
|
406
|
+
column.in(Array(compare))
|
|
407
|
+
when :null
|
|
408
|
+
handle_null_operator(column, compare)
|
|
409
|
+
end
|
|
410
|
+
end
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
def build_boolean_where_clause(key, value, target_klass)
|
|
414
|
+
column = target_klass.arel_table[key]
|
|
415
|
+
|
|
416
|
+
normalizer = ->(value) { [true, false, nil].include?(value) ? { eq: value } : value }
|
|
417
|
+
|
|
418
|
+
builder = Builder.new(column, key, allowed_types: [Hash])
|
|
419
|
+
|
|
420
|
+
builder.build(
|
|
421
|
+
value,
|
|
422
|
+
normalizer:,
|
|
423
|
+
valid_operators: Constants::NULLABLE_BOOLEAN_OPERATORS,
|
|
424
|
+
) do |operator, compare|
|
|
425
|
+
case operator
|
|
426
|
+
when :eq then column.eq(compare)
|
|
427
|
+
when :null then handle_null_operator(column, compare)
|
|
428
|
+
end
|
|
429
|
+
end
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
def handle_null_operator(column, compare)
|
|
433
|
+
compare ? column.eq(nil) : column.not_eq(nil)
|
|
434
|
+
end
|
|
435
|
+
|
|
436
|
+
def sqlite_adapter?
|
|
437
|
+
@sqlite_adapter ||= representation_class.model_class.connection.adapter_name == 'SQLite'
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
def case_sensitive_pattern_match(column, pattern)
|
|
441
|
+
if sqlite_adapter?
|
|
442
|
+
Arel::Nodes::InfixOperation.new('GLOB', column, Arel::Nodes.build_quoted(pattern.tr('%', '*')))
|
|
443
|
+
else
|
|
444
|
+
column.matches(pattern)
|
|
445
|
+
end
|
|
446
|
+
end
|
|
447
|
+
|
|
448
|
+
def separate_logical_operators(params)
|
|
449
|
+
[params.slice(*Constants::LOGICAL_OPERATORS), params.except(*Constants::LOGICAL_OPERATORS)]
|
|
450
|
+
end
|
|
451
|
+
|
|
452
|
+
def with_joins_and_distinct(scope, joins)
|
|
453
|
+
result = yield(joins.present? ? scope.joins(joins) : scope)
|
|
454
|
+
joins.present? ? result.distinct : result
|
|
455
|
+
end
|
|
456
|
+
end
|
|
457
|
+
end
|
|
458
|
+
end
|
|
459
|
+
end
|
|
460
|
+
end
|
|
461
|
+
end
|
|
462
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Apiwork
|
|
4
|
+
module Adapter
|
|
5
|
+
class Standard
|
|
6
|
+
module Capability
|
|
7
|
+
class Filtering
|
|
8
|
+
class Operation < Adapter::Capability::Operation::Base
|
|
9
|
+
target :collection
|
|
10
|
+
|
|
11
|
+
def apply
|
|
12
|
+
params = request.query[:filter]
|
|
13
|
+
return if params.blank?
|
|
14
|
+
|
|
15
|
+
result(**Filter.apply(data, representation_class, params))
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Apiwork
|
|
4
|
+
module Adapter
|
|
5
|
+
class Standard
|
|
6
|
+
module Capability
|
|
7
|
+
class Filtering < Adapter::Capability::Base
|
|
8
|
+
class RequestTransformer < Adapter::Capability::Transformer::Request::Base
|
|
9
|
+
NUMERIC_KEY_PATTERN = /^\d+$/
|
|
10
|
+
|
|
11
|
+
phase :before
|
|
12
|
+
|
|
13
|
+
def transform
|
|
14
|
+
request.transform(&method(:transform_value))
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
def transform_value(value)
|
|
20
|
+
case value
|
|
21
|
+
when Hash then apply(value)
|
|
22
|
+
when Array then value.map(&method(:transform_value))
|
|
23
|
+
else value
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def apply(hash)
|
|
28
|
+
return to_array(hash) if indexed_hash?(hash)
|
|
29
|
+
|
|
30
|
+
hash.transform_values(&method(:transform_value))
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def to_array(hash)
|
|
34
|
+
hash.keys.sort_by { |key| key.to_s.to_i }.map { |key| transform_value(hash[key]) }
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def indexed_hash?(hash)
|
|
38
|
+
return false if hash.empty?
|
|
39
|
+
|
|
40
|
+
hash.keys.all? { |key| NUMERIC_KEY_PATTERN.match?(key.to_s) }
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Apiwork
|
|
4
|
+
module Adapter
|
|
5
|
+
class Standard
|
|
6
|
+
module Capability
|
|
7
|
+
class Filtering < Adapter::Capability::Base
|
|
8
|
+
capability_name :filtering
|
|
9
|
+
|
|
10
|
+
request_transformer RequestTransformer
|
|
11
|
+
api_builder APIBuilder
|
|
12
|
+
contract_builder ContractBuilder
|
|
13
|
+
operation Operation
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|