hq-graphql 2.2.0 → 2.2.4

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 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: