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 +4 -4
- data/lib/hq/graphql/comparator.rb +24 -18
- data/lib/hq/graphql/enum/filter_operation.rb +15 -0
- data/lib/hq/graphql/ext/schema_extensions.rb +10 -10
- data/lib/hq/graphql/filter_operations.rb +59 -0
- data/lib/hq/graphql/filters.rb +150 -0
- data/lib/hq/graphql/resource.rb +48 -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 +6 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: deb4969d6bb5453570686309a4c86237970c3d273e8551a5749f8f686efb37be
|
4
|
+
data.tar.gz: ad22b68ed30b65621ff4190eb018e528eccd62180b5539bdde30efcf59c1f061
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
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
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
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
|
-
|
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 :
|
9
|
-
klass.alias_method :
|
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
|
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 @
|
34
|
-
while (args, options = @
|
35
|
-
|
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/
|
41
|
-
def
|
42
|
-
@
|
43
|
-
@
|
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
|
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"
|
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
|
-
|
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
|
data/lib/hq/graphql/version.rb
CHANGED
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.
|
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-
|
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:
|