hq-graphql 2.1.12 → 2.2.3

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