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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 716058e2edc04ba4af8f3ab85fb2af83dd73a89257d26e91f972edc0272de856
4
- data.tar.gz: 3c844bc5ba2473a40b43e3f4e591795eb6d6adb51e763066a53d42ffc3f55de0
3
+ metadata.gz: ad646bb38a0bd0920ca49428245d5183377719ef6b4802a0a619dc5c8bdbbcdf
4
+ data.tar.gz: 53f4c4b248e1fe424d34eb37e86002a18e6a39a54c91141f939d6aa9d85438db
5
5
  SHA512:
6
- metadata.gz: 518d168eda93dfccaacfcc4c50d0ec7b715db6053d9d41e5cc722078e3e967db75be980999f02bcf3e87b25667e0987205b0364065a817b60daa2ee6acfd0d4f
7
- data.tar.gz: 170ee8cd55160692280044b3ac3e84fc4b615a059fbc4f8f2cc38e2c401eee6cc7e49eb48c1743b102a6ae35c3112f4640fd7f8d0822816245104cb96e888a4d
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
@@ -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
- define_method(:resolve) do |limit: nil, offset: nil, sort_by: nil, sort_order: nil, **_attrs|
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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module HQ
4
4
  module GraphQL
5
- VERSION = "2.2.2"
5
+ VERSION = "2.2.3"
6
6
  end
7
7
  end
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.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-02-12 00:00:00.000000000 Z
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: