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.
- checksums.yaml +4 -4
- data/README.md +25 -9
- data/lib/hq/graphql.rb +17 -8
- data/lib/hq/graphql/association_loader.rb +1 -0
- data/lib/hq/graphql/comparator.rb +22 -21
- data/lib/hq/graphql/enum/filter_operation.rb +15 -0
- data/lib/hq/graphql/enum/sort_by.rb +8 -4
- data/lib/hq/graphql/enum/sort_order.rb +8 -4
- data/lib/hq/graphql/ext.rb +8 -0
- data/lib/hq/graphql/ext/active_record_extensions.rb +148 -0
- data/lib/hq/graphql/ext/enum_extensions.rb +81 -0
- data/lib/hq/graphql/ext/input_object_extensions.rb +110 -0
- data/lib/hq/graphql/ext/mutation_extensions.rb +24 -0
- data/lib/hq/graphql/ext/object_extensions.rb +122 -0
- data/lib/hq/graphql/ext/schema_extensions.rb +50 -0
- data/lib/hq/graphql/field.rb +1 -1
- data/lib/hq/graphql/filter_operations.rb +59 -0
- data/lib/hq/graphql/filters.rb +150 -0
- data/lib/hq/graphql/paginated_association_loader.rb +1 -0
- data/lib/hq/graphql/record_loader.rb +1 -0
- data/lib/hq/graphql/resource.rb +86 -15
- data/lib/hq/graphql/resource/auto_mutation.rb +8 -5
- data/lib/hq/graphql/root_mutation.rb +1 -1
- data/lib/hq/graphql/root_query.rb +3 -3
- data/lib/hq/graphql/scalars.rb +0 -2
- data/lib/hq/graphql/types.rb +1 -2
- data/lib/hq/graphql/types/uuid.rb +3 -5
- data/lib/hq/graphql/util.rb +13 -0
- data/lib/hq/graphql/version.rb +1 -1
- metadata +28 -38
- data/lib/hq/graphql/active_record_extensions.rb +0 -143
- data/lib/hq/graphql/enum.rb +0 -78
- data/lib/hq/graphql/input_object.rb +0 -104
- data/lib/hq/graphql/mutation.rb +0 -15
- data/lib/hq/graphql/object.rb +0 -120
- data/lib/hq/graphql/schema.rb +0 -22
- 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
|
data/lib/hq/graphql/resource.rb
CHANGED
@@ -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/
|
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
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
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
|
-
|
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 || ::
|
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(::
|
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
|
-
|
133
|
+
|
134
|
+
klass = Class.new(::GraphQL::Schema::Mutation) do
|
134
135
|
graphql_name gql_name
|
135
136
|
|
136
|
-
define_method(:ready?) do
|
137
|
-
super(
|
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, ::
|
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,13 +2,13 @@
|
|
2
2
|
|
3
3
|
module HQ
|
4
4
|
module GraphQL
|
5
|
-
class RootQuery < ::
|
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 |
|
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
|
data/lib/hq/graphql/scalars.rb
CHANGED
data/lib/hq/graphql/types.rb
CHANGED
@@ -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
|
-
::
|
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
|