hq-graphql 2.2.0 → 2.2.4

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: a158f615a83d3743a038f248bc82c04285631e6575f62b5124bbb5767ab6ede9
4
- data.tar.gz: 630bea394ce6e073b1f4ee2bd62047fcf46ba433357d1c759b99d7ecf350bfec
3
+ metadata.gz: deb4969d6bb5453570686309a4c86237970c3d273e8551a5749f8f686efb37be
4
+ data.tar.gz: ad22b68ed30b65621ff4190eb018e528eccd62180b5539bdde30efcf59c1f061
5
5
  SHA512:
6
- metadata.gz: b6076f33bc12f04a7e701f2deeb7420fc44792af3e99727029196d24d68280276a15c7c8d70d875de82c3ec48e41afdc13b5578a45fbcff2ddcc0edfa8971628
7
- data.tar.gz: c85f1b1af47ddee4f049a431277ca63bb1728ab74738fe8bc1bbc0f7b31f9dedd14f12fb850045f82ff301d83729d3f896628154e0fcaecea17c117dfd6140d5
6
+ metadata.gz: 7a2a1f0e2f88cdda2142055d40228390a7f7dd45cc5d503b8b5bfb3b49e7053a7ff14d54d240d04750d9d72f888011f04885731f914e09dde4ddfd538536ea5b
7
+ data.tar.gz: bcf822fdeb159f53a94c79d43204055ce3eff8297c007bd420f9164478a08442678d93a55fcaf943a5ff7f0adf41075bc7f4cebae745b4f523bc61ab21a3233c
@@ -13,26 +13,32 @@ module HQ
13
13
  non_breaking: 2
14
14
  }
15
15
 
16
- class << self
17
- def compare(old_schema, new_schema, criticality: :breaking)
18
- old_schema.load_types! if old_schema < ::GraphQL::Schema
19
- new_schema.load_types! if old_schema < ::GraphQL::Schema
20
- level = CRITICALITY[criticality]
21
- raise ::ArgumentError, "Invalid criticality. Possible values are #{CRITICALITY.keys.join(", ")}" unless level
16
+ def self.compare(old_schema, new_schema, criticality: :breaking)
17
+ level = CRITICALITY[criticality]
18
+ raise ::ArgumentError, "Invalid criticality. Possible values are #{CRITICALITY.keys.join(", ")}" unless level
19
+
20
+ result = ::GraphQL::SchemaComparator.compare(prepare_schema(old_schema), prepare_schema(new_schema))
21
+ return if result.identical?
22
+ changes = {}
23
+ changes[:breaking] = result.breaking_changes
24
+ if level >= CRITICALITY[:dangerous]
25
+ changes[:dangerous] = result.dangerous_changes
26
+ end
27
+ if level >= CRITICALITY[:non_breaking]
28
+ changes[:non_breaking] = result.non_breaking_changes
29
+ end
30
+ return unless changes.values.flatten.any?
22
31
 
23
- result = ::GraphQL::SchemaComparator.compare(old_schema, new_schema)
24
- return nil if result.identical?
25
- changes = {}
26
- changes[:breaking] = result.breaking_changes
27
- if level >= CRITICALITY[:dangerous]
28
- changes[:dangerous] = result.dangerous_changes
29
- end
30
- if level >= CRITICALITY[:non_breaking]
31
- changes[:non_breaking] = result.non_breaking_changes
32
- end
33
- return nil unless changes.values.flatten.any?
32
+ changes
33
+ end
34
+
35
+ class << self
36
+ private
34
37
 
35
- changes
38
+ def prepare_schema(schema)
39
+ schema = ::GraphQL::Schema.from_definition(schema) if schema.is_a?(String)
40
+ schema.load_types!
41
+ schema
36
42
  end
37
43
  end
38
44
  end
@@ -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
@@ -5,11 +5,11 @@ module HQ
5
5
  module Ext
6
6
  module SchemaExtensions
7
7
  def self.prepended(klass)
8
- klass.alias_method :add_type_without_lazyload, :add_type
9
- klass.alias_method :add_type, :add_type_with_lazyload
8
+ klass.alias_method :add_type_and_traverse_without_types, :add_type_and_traverse
9
+ klass.alias_method :add_type_and_traverse, :add_type_and_traverse_with_types
10
10
  end
11
11
 
12
- def execute(*args, **options)
12
+ def multiplex(*args, **options)
13
13
  load_types!
14
14
  super
15
15
  end
@@ -30,17 +30,17 @@ module HQ
30
30
 
31
31
  def load_types!
32
32
  ::HQ::GraphQL.load_types!
33
- return if @add_type_with_lazyload.blank?
34
- while (args, options = @add_type_with_lazyload.shift)
35
- add_type_without_lazyload(*args, **options)
33
+ return if @add_type_and_traverse_with_types.blank?
34
+ while (args, options = @add_type_and_traverse_with_types.shift)
35
+ add_type_and_traverse_without_types(*args, **options)
36
36
  end
37
37
  end
38
38
 
39
39
  # Defer adding types until first schema execution
40
- # https://github.com/rmosolgo/graphql-ruby/blob/792f276444e1dd6004fcafe3820d65fdbbe285f0/lib/graphql/schema.rb#L1888-L1980
41
- def add_type_with_lazyload(*args, **options)
42
- @add_type_with_lazyload ||= []
43
- @add_type_with_lazyload.push([args, options])
40
+ # https://github.com/rmosolgo/graphql-ruby/blob/345ebb2e3833909963067d81e0e8378717b5bdbf/lib/graphql/schema.rb#L1792
41
+ def add_type_and_traverse_with_types(*args, **options)
42
+ @add_type_and_traverse_with_types ||= []
43
+ @add_type_and_traverse_with_types.push([args, options])
44
44
  end
45
45
  end
46
46
  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.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.0"
5
+ VERSION = "2.2.4"
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.0
4
+ version: 2.2.4
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-01 00:00:00.000000000 Z
11
+ date: 2021-10-11 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: