hq-graphql 2.1.12 → 2.2.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +25 -9
  3. data/lib/hq/graphql.rb +17 -8
  4. data/lib/hq/graphql/association_loader.rb +1 -0
  5. data/lib/hq/graphql/comparator.rb +22 -21
  6. data/lib/hq/graphql/enum/filter_operation.rb +15 -0
  7. data/lib/hq/graphql/enum/sort_by.rb +8 -4
  8. data/lib/hq/graphql/enum/sort_order.rb +8 -4
  9. data/lib/hq/graphql/ext.rb +8 -0
  10. data/lib/hq/graphql/ext/active_record_extensions.rb +148 -0
  11. data/lib/hq/graphql/ext/enum_extensions.rb +81 -0
  12. data/lib/hq/graphql/ext/input_object_extensions.rb +110 -0
  13. data/lib/hq/graphql/ext/mutation_extensions.rb +24 -0
  14. data/lib/hq/graphql/ext/object_extensions.rb +122 -0
  15. data/lib/hq/graphql/ext/schema_extensions.rb +50 -0
  16. data/lib/hq/graphql/field.rb +1 -1
  17. data/lib/hq/graphql/filter_operations.rb +59 -0
  18. data/lib/hq/graphql/filters.rb +150 -0
  19. data/lib/hq/graphql/paginated_association_loader.rb +1 -0
  20. data/lib/hq/graphql/record_loader.rb +1 -0
  21. data/lib/hq/graphql/resource.rb +86 -15
  22. data/lib/hq/graphql/resource/auto_mutation.rb +8 -5
  23. data/lib/hq/graphql/root_mutation.rb +1 -1
  24. data/lib/hq/graphql/root_query.rb +3 -3
  25. data/lib/hq/graphql/scalars.rb +0 -2
  26. data/lib/hq/graphql/types.rb +1 -2
  27. data/lib/hq/graphql/types/uuid.rb +3 -5
  28. data/lib/hq/graphql/util.rb +13 -0
  29. data/lib/hq/graphql/version.rb +1 -1
  30. metadata +28 -38
  31. data/lib/hq/graphql/active_record_extensions.rb +0 -143
  32. data/lib/hq/graphql/enum.rb +0 -78
  33. data/lib/hq/graphql/input_object.rb +0 -104
  34. data/lib/hq/graphql/mutation.rb +0 -15
  35. data/lib/hq/graphql/object.rb +0 -120
  36. data/lib/hq/graphql/schema.rb +0 -22
  37. data/lib/hq/graphql/types/object.rb +0 -35
@@ -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
@@ -17,6 +17,7 @@ module HQ
17
17
  end
18
18
 
19
19
  def initialize(model, association_name, internal_association: false, limit: nil, offset: nil, scope: nil, sort_by: nil, sort_order: nil)
20
+ super()
20
21
  @model = model
21
22
  @association_name = association_name
22
23
  @internal_association = internal_association
@@ -4,6 +4,7 @@ module HQ
4
4
  module GraphQL
5
5
  class RecordLoader < ::GraphQL::Batch::Loader
6
6
  def initialize(model, column: model.primary_key, where: nil)
7
+ super()
7
8
  @model = model
8
9
  @column = column.to_s
9
10
  @column_type = model.type_for_attribute(@column)
@@ -1,9 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "hq/graphql/ext/enum_extensions"
4
+ require "hq/graphql/ext/input_object_extensions"
5
+ require "hq/graphql/ext/object_extensions"
6
+ require "hq/graphql/enum/filter_operation"
3
7
  require "hq/graphql/enum/sort_by"
4
8
  require "hq/graphql/field_extension/paginated_arguments"
5
- require "hq/graphql/input_object"
6
- require "hq/graphql/object"
9
+ require "hq/graphql/filters"
7
10
  require "hq/graphql/resource/auto_mutation"
8
11
  require "hq/graphql/scalars"
9
12
 
@@ -60,18 +63,21 @@ module HQ
60
63
  end
61
64
 
62
65
  def nil_query_object
63
- @nil_query_object ||= build_graphql_object(name: "#{graphql_name}Copy", auto_nil: false)
66
+ @nil_query_object ||= const_set(:NilQuery, build_graphql_object(name: "#{graphql_name}Copy", auto_nil: false))
64
67
  end
65
68
 
66
69
  def query_object
67
70
  @query_object ||= begin
68
- if @query_object_options
69
- options, block = @query_object_options
70
- @query_object_options = nil
71
- build_graphql_object(**options, &block)
72
- else
73
- build_graphql_object
74
- end
71
+ qo =
72
+ if @query_object_options
73
+ options, block = @query_object_options
74
+ @query_object_options = nil
75
+ build_graphql_object(**options, &block)
76
+ else
77
+ build_graphql_object
78
+ end
79
+ remove_const(:Query) if const_defined?(:Query, false)
80
+ const_set(:Query, qo)
75
81
  end
76
82
  end
77
83
 
@@ -79,6 +85,59 @@ module HQ
79
85
  @sort_fields_enum || ::HQ::GraphQL::Enum::SortBy
80
86
  end
81
87
 
88
+ def const_missing(constant_name)
89
+ constant_name = constant_name.to_sym
90
+ case constant_name
91
+ when :Query
92
+ query_object
93
+ when :NilQuery
94
+ nil_query_object
95
+ when :Input
96
+ input_klass
97
+ when :FilterInput
98
+ filter_input
99
+ when :FilterFields
100
+ filter_fields_enum
101
+ else
102
+ super
103
+ end
104
+ end
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
+
82
141
  protected
83
142
 
84
143
  def default_scope(&block)
@@ -115,11 +174,15 @@ module HQ
115
174
  def def_root(field_name, is_array: false, null: true, &block)
116
175
  resource = self
117
176
  resolver = -> {
118
- Class.new(::GraphQL::Schema::Resolver) do
177
+ klass = Class.new(::GraphQL::Schema::Resolver) do
119
178
  type = is_array ? [resource.query_object] : resource.query_object
120
179
  type type, null: null
121
180
  class_eval(&block) if block
122
181
  end
182
+
183
+ constant_name = "#{field_name.to_s.classify}Resolver"
184
+ resource.send(:remove_const, constant_name) if resource.const_defined?(constant_name, false)
185
+ resource.const_set(constant_name, klass)
123
186
  }
124
187
  ::HQ::GraphQL.root_queries << {
125
188
  field_name: field_name, resolver: resolver, model_name: model_name
@@ -146,9 +209,13 @@ module HQ
146
209
  if find_all
147
210
  def_root field_name.pluralize, is_array: true, null: false do
148
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!
149
217
 
150
- define_method(:resolve) do |limit: nil, offset: nil, sort_by: nil, sort_order: nil, **_attrs|
151
- scope = scoped_self.scope(context).all
218
+ scope = scoped_self.scope(context).all.merge(filters_scope.to_scope)
152
219
 
153
220
  if pagination || page || limit
154
221
  offset = [0, *offset].max
@@ -172,7 +239,7 @@ module HQ
172
239
  def build_graphql_object(name: graphql_name, **options, &block)
173
240
  scoped_graphql_name = name
174
241
  scoped_model_name = model_name
175
- object_class = @query_class || ::HQ::GraphQL.default_object_class || ::HQ::GraphQL::Object
242
+ object_class = @query_class || ::HQ::GraphQL.default_object_class || ::GraphQL::Schema::Object
176
243
  Class.new(object_class) do
177
244
  graphql_name scoped_graphql_name
178
245
 
@@ -187,18 +254,22 @@ module HQ
187
254
  scoped_model_name = model_name
188
255
  scoped_excluded_inputs = @excluded_inputs || []
189
256
 
190
- Class.new(::HQ::GraphQL::InputObject) do
257
+ input_klass = Class.new(::GraphQL::Schema::InputObject) do
191
258
  graphql_name "#{scoped_graphql_name}Input"
192
259
 
193
260
  with_model scoped_model_name, excluded_inputs: scoped_excluded_inputs, **options
194
261
 
195
262
  class_eval(&block) if block
196
263
  end
264
+
265
+ remove_const(:Input) if const_defined?(:Input, false)
266
+ const_set(:Input, input_klass)
197
267
  end
198
268
 
199
269
  def sort_fields_enum=(fields)
200
270
  @sort_fields_enum ||= Class.new(::HQ::GraphQL::Enum::SortBy).tap do |c|
201
271
  c.graphql_name "#{graphql_name}Sort"
272
+ const_set(:Sort, c)
202
273
  end
203
274
 
204
275
  Array(fields).each do |field|
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "hq/graphql/ext/mutation_extensions"
3
4
  require "hq/graphql/inputs"
4
- require "hq/graphql/mutation"
5
5
  require "hq/graphql/types"
6
6
 
7
7
  module HQ
@@ -130,15 +130,16 @@ module HQ
130
130
  def build_mutation(action:, require_primary_key: false, nil_klass: false, &block)
131
131
  gql_name = "#{graphql_name}#{action.to_s.titleize}"
132
132
  scoped_model_name = model_name
133
- Class.new(::HQ::GraphQL::Mutation) do
133
+
134
+ klass = Class.new(::GraphQL::Schema::Mutation) do
134
135
  graphql_name gql_name
135
136
 
136
- define_method(:ready?) do |*args|
137
- super(*args) && ::HQ::GraphQL.authorized?(action, scoped_model_name, context)
137
+ define_method(:ready?) do |**args|
138
+ super(**args) && ::HQ::GraphQL.authorized?(action, scoped_model_name, context)
138
139
  end
139
140
 
140
141
  lazy_load do
141
- field :errors, ::HQ::GraphQL::Types::Object, null: false
142
+ field :errors, ::GraphQL::Types::JSON, null: false
142
143
  field :resource, ::HQ::GraphQL::Types[scoped_model_name, nil_klass], null: true
143
144
  end
144
145
 
@@ -156,6 +157,8 @@ module HQ
156
157
  resource.errors.to_h.deep_transform_keys { |k| k.to_s.camelize(:lower) }
157
158
  end
158
159
  end
160
+
161
+ const_set(gql_name, klass)
159
162
  end
160
163
  end
161
164
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  module HQ
4
4
  module GraphQL
5
- class RootMutation < ::HQ::GraphQL::Object
5
+ class RootMutation < ::GraphQL::Schema::Object
6
6
  def self.inherited(base)
7
7
  super
8
8
  base.class_eval do
@@ -2,13 +2,13 @@
2
2
 
3
3
  module HQ
4
4
  module GraphQL
5
- class RootQuery < ::HQ::GraphQL::Object
5
+ class RootQuery < ::GraphQL::Schema::Object
6
6
  def self.inherited(base)
7
7
  super
8
8
  base.class_eval do
9
9
  lazy_load do
10
- ::HQ::GraphQL.root_queries.each do |field_name:, resolver:, model_name:|
11
- field field_name, resolver: resolver.call, klass: model_name
10
+ ::HQ::GraphQL.root_queries.each do |query|
11
+ field query[:field_name], resolver: query[:resolver].call, klass: query[:model_name]
12
12
  end
13
13
  end
14
14
  end
@@ -1,12 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "hq/graphql/types/object"
4
3
  require "hq/graphql/types/uuid"
5
4
 
6
5
  module HQ
7
6
  module GraphQL
8
7
  module Scalars
9
- Object = ::HQ::GraphQL::Types::Object
10
8
  UUID = ::HQ::GraphQL::Types::UUID
11
9
  end
12
10
  end
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "hq/graphql/types/object"
4
3
  require "hq/graphql/types/uuid"
5
4
 
6
5
  module HQ
@@ -35,7 +34,7 @@ module HQ
35
34
  when :uuid
36
35
  ::HQ::GraphQL::Types::UUID
37
36
  when :json, :jsonb
38
- ::HQ::GraphQL::Types::Object
37
+ ::GraphQL::Types::JSON
39
38
  when :integer
40
39
  ::GraphQL::Types::Int
41
40
  when :decimal
@@ -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