hq-graphql 2.2.2 → 2.2.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/hq/graphql/enum/filter_operation.rb +15 -0
- data/lib/hq/graphql/filter_operations.rb +59 -0
- data/lib/hq/graphql/filters.rb +150 -0
- data/lib/hq/graphql/resource.rb +48 -2
- data/lib/hq/graphql/types/uuid.rb +3 -5
- data/lib/hq/graphql/util.rb +13 -0
- data/lib/hq/graphql/version.rb +1 -1
- metadata +6 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ad646bb38a0bd0920ca49428245d5183377719ef6b4802a0a619dc5c8bdbbcdf
|
4
|
+
data.tar.gz: 53f4c4b248e1fe424d34eb37e86002a18e6a39a54c91141f939d6aa9d85438db
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c5978e1805f3097aaf708f667fbfb669b74205edc8c4f73f7e859f768636a41e6fd72de00dc62322e90a6e127850467b7bf0a9ebd82507f50b197625259cf7cd
|
7
|
+
data.tar.gz: fc94b8611372d29218b621e762ccbfac2de279a5b16276eeaf8043a8f9f19a03539ceb90c9270fe22b306b4a3191c7ab7e8e50bcf6d989d512ff3db94e23edd1
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "hq/graphql/filter_operations"
|
4
|
+
|
5
|
+
module HQ
|
6
|
+
module GraphQL
|
7
|
+
module Enum
|
8
|
+
class FilterOperation < ::GraphQL::Schema::Enum
|
9
|
+
::HQ::GraphQL::FilterOperations::OPERATIONS.each do |filter_operation|
|
10
|
+
value filter_operation.name, value: filter_operation
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "hq/graphql/enum/filter_operation"
|
4
|
+
|
5
|
+
module HQ
|
6
|
+
module GraphQL
|
7
|
+
module FilterOperations
|
8
|
+
class Operation
|
9
|
+
attr_reader :name, :arel, :check_for_null, :sanitize
|
10
|
+
|
11
|
+
def initialize(name:, arel:, check_for_null: false, sanitize: nil)
|
12
|
+
@name = name
|
13
|
+
@arel = arel
|
14
|
+
@check_for_null = check_for_null
|
15
|
+
@sanitize = sanitize
|
16
|
+
end
|
17
|
+
|
18
|
+
def sanitize_value(value)
|
19
|
+
sanitize ? sanitize.call(value) : value
|
20
|
+
end
|
21
|
+
|
22
|
+
def to_arel(table:, column_name:, value:)
|
23
|
+
sanitized_value = sanitize_value(value)
|
24
|
+
|
25
|
+
if arel.is_a?(Proc)
|
26
|
+
return arel.call(table: table, column_name: column_name, value: sanitized_value)
|
27
|
+
end
|
28
|
+
|
29
|
+
expression = table[column_name].send(arel, sanitized_value)
|
30
|
+
|
31
|
+
if check_for_null
|
32
|
+
expression = expression.or(table[column_name].eq(nil))
|
33
|
+
end
|
34
|
+
|
35
|
+
expression
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
OPERATIONS = [
|
40
|
+
EQUAL = Operation.new(name: "EQUAL", arel: :eq),
|
41
|
+
NOT_EQUAL = Operation.new(name: "NOT_EQUAL", arel: :not_eq, check_for_null: true),
|
42
|
+
LIKE = Operation.new(name: "LIKE", arel: :matches, sanitize: ->(value) { "%#{ActiveRecord::Base.sanitize_sql_like(value)}%" }),
|
43
|
+
NOT_LIKE = Operation.new(name: "NOT_LIKE", arel: :does_not_match, check_for_null: true, sanitize: ->(value) { "%#{ActiveRecord::Base.sanitize_sql_like(value)}%" }),
|
44
|
+
GREATER_THAN = Operation.new(name: "GREATER_THAN", arel: :gt),
|
45
|
+
LESS_THAN = Operation.new(name: "LESS_THAN", arel: :lt),
|
46
|
+
WITH = Operation.new(
|
47
|
+
name: "WITH",
|
48
|
+
arel: ->(table:, column_name:, value:) do
|
49
|
+
if value.casecmp("t") == 0 || value.casecmp("true") == 0
|
50
|
+
table[column_name].not_eq(nil)
|
51
|
+
else
|
52
|
+
table[column_name].eq(nil)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
)
|
56
|
+
]
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,150 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "hq/graphql/filter_operations"
|
4
|
+
require "hq/graphql/util"
|
5
|
+
|
6
|
+
module HQ
|
7
|
+
module GraphQL
|
8
|
+
class Filters
|
9
|
+
BOOLEAN_VALUES = ["t", "f", "true", "false"]
|
10
|
+
|
11
|
+
def self.supported?(column)
|
12
|
+
!!Filter.class_from_column(column)
|
13
|
+
end
|
14
|
+
|
15
|
+
attr_reader :filters, :model
|
16
|
+
|
17
|
+
def initialize(filters, model)
|
18
|
+
@filters = Array(filters).map { |filter| Filter.for(filter, table: model.arel_table) }
|
19
|
+
@model = model
|
20
|
+
end
|
21
|
+
|
22
|
+
def validate!
|
23
|
+
filters.each(&:validate)
|
24
|
+
errors = filters.map do |filter|
|
25
|
+
filter.display_error_message
|
26
|
+
end.flatten.uniq
|
27
|
+
|
28
|
+
if errors.any?
|
29
|
+
raise ::GraphQL::ExecutionError, errors.join(", ")
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def to_scope
|
34
|
+
filters.reduce(model.all) do |s, filter|
|
35
|
+
s.where(filter.to_arel)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
class Filter
|
40
|
+
include ActiveModel::Validations
|
41
|
+
include FilterOperations
|
42
|
+
|
43
|
+
def self.for(filter, **options)
|
44
|
+
class_from_column(filter.field).new(filter, **options)
|
45
|
+
end
|
46
|
+
|
47
|
+
def self.class_from_column(column)
|
48
|
+
case column.type
|
49
|
+
when :boolean
|
50
|
+
BooleanFilter
|
51
|
+
when :date, :datetime
|
52
|
+
DateFilter
|
53
|
+
when :decimal, :integer
|
54
|
+
NumericFilter
|
55
|
+
when :string, :text
|
56
|
+
StringFilter
|
57
|
+
when :uuid
|
58
|
+
UuidFilter
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def self.validate_operations(*operations)
|
63
|
+
valid_operations = operations + [WITH]
|
64
|
+
validates :operation, inclusion: {
|
65
|
+
in: valid_operations,
|
66
|
+
message: "only supports the following operations: #{valid_operations.map(&:name).join(", ")}"
|
67
|
+
}
|
68
|
+
end
|
69
|
+
|
70
|
+
def self.validate_value(**options)
|
71
|
+
validates :value, **options, unless: ->(filter) { filter.operation == WITH }
|
72
|
+
end
|
73
|
+
|
74
|
+
validate :validate_boolean_values, if: ->(filter) { filter.operation == WITH }
|
75
|
+
|
76
|
+
attr_reader :table, :column, :operation, :value
|
77
|
+
|
78
|
+
def initialize(filter, table:)
|
79
|
+
@table = table
|
80
|
+
@column = filter.field
|
81
|
+
@operation = filter.operation
|
82
|
+
@value = filter.value
|
83
|
+
end
|
84
|
+
|
85
|
+
def display_error_message
|
86
|
+
return unless errors.any?
|
87
|
+
messages = errors.messages.values.join(", ")
|
88
|
+
"#{column.name.camelize(:lower)} (type: #{column.type}, operation: #{operation.name}, value: \"#{value}\"): #{messages}"
|
89
|
+
end
|
90
|
+
|
91
|
+
def to_arel
|
92
|
+
operation.to_arel(table: table, column_name: column.name, value: value)
|
93
|
+
end
|
94
|
+
|
95
|
+
def validate_boolean_values
|
96
|
+
is_valid = BOOLEAN_VALUES.any? { |v| value.casecmp(v) == 0 }
|
97
|
+
return if is_valid
|
98
|
+
errors.add(:value, "WITH operation only supports boolean values (#{BOOLEAN_VALUES.join(", ")})")
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
class BooleanFilter < Filter
|
103
|
+
validate_operations
|
104
|
+
|
105
|
+
def to_arel
|
106
|
+
arel = super
|
107
|
+
|
108
|
+
if value.casecmp("f") == 0 || value.casecmp("false") == 0
|
109
|
+
arel = arel.or(table[column.name].eq(false))
|
110
|
+
end
|
111
|
+
|
112
|
+
arel
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
class DateFilter < Filter
|
117
|
+
validate_operations GREATER_THAN, LESS_THAN
|
118
|
+
validate :validate_iso8601
|
119
|
+
|
120
|
+
def validate_iso8601
|
121
|
+
is_valid = begin
|
122
|
+
DateTime.iso8601(value)
|
123
|
+
true
|
124
|
+
rescue ArgumentError
|
125
|
+
false
|
126
|
+
end
|
127
|
+
|
128
|
+
return if is_valid
|
129
|
+
|
130
|
+
today = Date.today
|
131
|
+
errors.add(:value, "only supports ISO8601 values (\"#{today.iso8601}\", \"#{today.to_datetime.iso8601}\")")
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
class NumericFilter < Filter
|
136
|
+
validate_operations GREATER_THAN, LESS_THAN, EQUAL, NOT_EQUAL
|
137
|
+
validate_value numericality: { message: "only supports numerical values" }
|
138
|
+
end
|
139
|
+
|
140
|
+
class StringFilter < Filter
|
141
|
+
validate_operations EQUAL, NOT_EQUAL, LIKE, NOT_LIKE
|
142
|
+
end
|
143
|
+
|
144
|
+
class UuidFilter < Filter
|
145
|
+
validate_operations EQUAL, NOT_EQUAL
|
146
|
+
validate_value format: { with: HQ::GraphQL::Util::UUID_FORMAT, message: "only supports UUID values (e.g. 00000000-0000-0000-0000-000000000000)" }
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
data/lib/hq/graphql/resource.rb
CHANGED
@@ -1,9 +1,12 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "hq/graphql/ext/enum_extensions"
|
3
4
|
require "hq/graphql/ext/input_object_extensions"
|
4
5
|
require "hq/graphql/ext/object_extensions"
|
6
|
+
require "hq/graphql/enum/filter_operation"
|
5
7
|
require "hq/graphql/enum/sort_by"
|
6
8
|
require "hq/graphql/field_extension/paginated_arguments"
|
9
|
+
require "hq/graphql/filters"
|
7
10
|
require "hq/graphql/resource/auto_mutation"
|
8
11
|
require "hq/graphql/scalars"
|
9
12
|
|
@@ -91,11 +94,50 @@ module HQ
|
|
91
94
|
nil_query_object
|
92
95
|
when :Input
|
93
96
|
input_klass
|
97
|
+
when :FilterInput
|
98
|
+
filter_input
|
99
|
+
when :FilterFields
|
100
|
+
filter_fields_enum
|
94
101
|
else
|
95
102
|
super
|
96
103
|
end
|
97
104
|
end
|
98
105
|
|
106
|
+
def filter_input
|
107
|
+
@filter_input ||= begin
|
108
|
+
scoped_self = self
|
109
|
+
|
110
|
+
input_class = Class.new(::GraphQL::Schema::InputObject) do
|
111
|
+
graphql_name "#{scoped_self.graphql_name}FilterInput"
|
112
|
+
|
113
|
+
argument :field, scoped_self.filter_fields_enum, required: true
|
114
|
+
argument :operation, Enum::FilterOperation, required: true
|
115
|
+
argument :value, String, required: true
|
116
|
+
end
|
117
|
+
|
118
|
+
const_set(:FilterInput, input_class)
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
def filter_fields_enum
|
123
|
+
@filter_fields_enum ||= begin
|
124
|
+
scoped_self = self
|
125
|
+
|
126
|
+
enum_class = Class.new(::GraphQL::Schema::Enum) do
|
127
|
+
graphql_name "#{scoped_self.graphql_name}FilterFields"
|
128
|
+
|
129
|
+
lazy_load do
|
130
|
+
scoped_self.model_klass.columns.sort_by(&:name).each do |column|
|
131
|
+
next unless HQ::GraphQL::Filters.supported?(column)
|
132
|
+
value column.name.camelize(:lower), value: column
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
const_set(:FilterFields, enum_class)
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
99
141
|
protected
|
100
142
|
|
101
143
|
def default_scope(&block)
|
@@ -167,9 +209,13 @@ module HQ
|
|
167
209
|
if find_all
|
168
210
|
def_root field_name.pluralize, is_array: true, null: false do
|
169
211
|
extension FieldExtension::PaginatedArguments, klass: scoped_self.model_klass if pagination
|
212
|
+
argument :filters, [scoped_self.filter_input], required: false
|
213
|
+
|
214
|
+
define_method(:resolve) do |filters: nil, limit: nil, offset: nil, sort_by: nil, sort_order: nil, **_attrs|
|
215
|
+
filters_scope = ::HQ::GraphQL::Filters.new(filters, scoped_self.model_klass)
|
216
|
+
filters_scope.validate!
|
170
217
|
|
171
|
-
|
172
|
-
scope = scoped_self.scope(context).all
|
218
|
+
scope = scoped_self.scope(context).all.merge(filters_scope.to_scope)
|
173
219
|
|
174
220
|
if pagination || page || limit
|
175
221
|
offset = [0, *offset].max
|
@@ -1,5 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "hq/graphql/util"
|
4
|
+
|
3
5
|
module HQ
|
4
6
|
module GraphQL
|
5
7
|
module Types
|
@@ -18,16 +20,12 @@ module HQ
|
|
18
20
|
private
|
19
21
|
|
20
22
|
def validate_and_return_uuid(value)
|
21
|
-
if validate_uuid(value)
|
23
|
+
if ::HQ::GraphQL::Util.validate_uuid(value)
|
22
24
|
value
|
23
25
|
else
|
24
26
|
raise ::GraphQL::CoercionError, "#{value.inspect} is not a valid UUID"
|
25
27
|
end
|
26
28
|
end
|
27
|
-
|
28
|
-
def validate_uuid(value)
|
29
|
-
!value || !!value.to_s.match(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/)
|
30
|
-
end
|
31
29
|
end
|
32
30
|
end
|
33
31
|
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module HQ
|
4
|
+
module GraphQL
|
5
|
+
module Util
|
6
|
+
UUID_FORMAT = /\A[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\z/
|
7
|
+
|
8
|
+
def self.validate_uuid(value)
|
9
|
+
!value || !!value.to_s.match(UUID_FORMAT)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
data/lib/hq/graphql/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: hq-graphql
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.2.
|
4
|
+
version: 2.2.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Danny Jones
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-
|
11
|
+
date: 2021-07-21 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|
@@ -236,6 +236,7 @@ files:
|
|
236
236
|
- lib/hq/graphql/comparator.rb
|
237
237
|
- lib/hq/graphql/config.rb
|
238
238
|
- lib/hq/graphql/engine.rb
|
239
|
+
- lib/hq/graphql/enum/filter_operation.rb
|
239
240
|
- lib/hq/graphql/enum/sort_by.rb
|
240
241
|
- lib/hq/graphql/enum/sort_order.rb
|
241
242
|
- lib/hq/graphql/ext.rb
|
@@ -249,6 +250,8 @@ files:
|
|
249
250
|
- lib/hq/graphql/field_extension/association_loader_extension.rb
|
250
251
|
- lib/hq/graphql/field_extension/paginated_arguments.rb
|
251
252
|
- lib/hq/graphql/field_extension/paginated_loader.rb
|
253
|
+
- lib/hq/graphql/filter_operations.rb
|
254
|
+
- lib/hq/graphql/filters.rb
|
252
255
|
- lib/hq/graphql/inputs.rb
|
253
256
|
- lib/hq/graphql/object_association.rb
|
254
257
|
- lib/hq/graphql/paginated_association_loader.rb
|
@@ -260,6 +263,7 @@ files:
|
|
260
263
|
- lib/hq/graphql/scalars.rb
|
261
264
|
- lib/hq/graphql/types.rb
|
262
265
|
- lib/hq/graphql/types/uuid.rb
|
266
|
+
- lib/hq/graphql/util.rb
|
263
267
|
- lib/hq/graphql/version.rb
|
264
268
|
homepage: https://github.com/OneHQ/hq-graphql
|
265
269
|
licenses:
|