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