hq-graphql 2.1.12 → 2.2.0
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 +3 -8
- 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/paginated_association_loader.rb +1 -0
- data/lib/hq/graphql/record_loader.rb +1 -0
- data/lib/hq/graphql/resource.rb +38 -13
- 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/version.rb +1 -1
- metadata +24 -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,110 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "hq/graphql/ext/active_record_extensions"
|
4
|
+
require "hq/graphql/inputs"
|
5
|
+
require "hq/graphql/types"
|
6
|
+
|
7
|
+
module HQ
|
8
|
+
module GraphQL
|
9
|
+
module Ext
|
10
|
+
module InputObjectExtensions
|
11
|
+
def self.included(klass)
|
12
|
+
klass.include Scalars
|
13
|
+
klass.include InstanceMethods
|
14
|
+
klass.include ActiveRecordExtensions
|
15
|
+
klass.extend ActiveRecordExtensions
|
16
|
+
klass.extend ClassMethods
|
17
|
+
end
|
18
|
+
|
19
|
+
module InstanceMethods
|
20
|
+
# Recursively format attributes so that they are compatible with `accepts_nested_attributes_for`
|
21
|
+
def format_nested_attributes
|
22
|
+
self.each.inject({}) do |formatted_attrs, (key, value) |
|
23
|
+
if self.class.nested_attributes.include?(key.to_s)
|
24
|
+
formatted_value =
|
25
|
+
if value.is_a?(Array)
|
26
|
+
value.map(&:format_nested_attributes)
|
27
|
+
elsif value
|
28
|
+
value.format_nested_attributes
|
29
|
+
end
|
30
|
+
|
31
|
+
formatted_attrs[:"#{key}_attributes"] = formatted_value if formatted_value
|
32
|
+
elsif key.to_s == "x"
|
33
|
+
formatted_attrs[:X] = value
|
34
|
+
else
|
35
|
+
formatted_attrs[key] = value
|
36
|
+
end
|
37
|
+
formatted_attrs
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
module ClassMethods
|
43
|
+
#### Class Methods ####
|
44
|
+
def with_model(model_name, attributes: true, associations: false, enums: true, excluded_inputs: [])
|
45
|
+
self.model_name = model_name
|
46
|
+
self.auto_load_attributes = attributes
|
47
|
+
self.auto_load_associations = associations
|
48
|
+
self.auto_load_enums = enums
|
49
|
+
|
50
|
+
lazy_load do
|
51
|
+
excluded_inputs += ::HQ::GraphQL.excluded_inputs
|
52
|
+
|
53
|
+
model_columns.each do |column|
|
54
|
+
argument_from_column(column) unless excluded_inputs.include?(column.name.to_sym)
|
55
|
+
end
|
56
|
+
|
57
|
+
model_associations.each do |association|
|
58
|
+
argument_from_association(association) unless excluded_inputs.include?(association.name.to_sym)
|
59
|
+
end
|
60
|
+
|
61
|
+
argument :X, String, required: false
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def nested_attributes
|
66
|
+
@nested_attributes ||= Set.new
|
67
|
+
end
|
68
|
+
|
69
|
+
private
|
70
|
+
|
71
|
+
def argument_from_association(association)
|
72
|
+
is_enum = is_enum?(association)
|
73
|
+
input_or_type = is_enum ? ::HQ::GraphQL::Types[association.klass] : ::HQ::GraphQL::Inputs[association.klass]
|
74
|
+
name = association.name
|
75
|
+
return if argument_exists?(name)
|
76
|
+
|
77
|
+
case association.macro
|
78
|
+
when :has_many
|
79
|
+
argument name, [input_or_type], required: false
|
80
|
+
else
|
81
|
+
argument name, input_or_type, required: false
|
82
|
+
end
|
83
|
+
|
84
|
+
return if is_enum
|
85
|
+
|
86
|
+
if !model_klass.nested_attributes_options.key?(name.to_sym)
|
87
|
+
model_klass.accepts_nested_attributes_for name, allow_destroy: true
|
88
|
+
end
|
89
|
+
|
90
|
+
nested_attributes << name.to_s
|
91
|
+
rescue ::HQ::GraphQL::Inputs::Error
|
92
|
+
nil
|
93
|
+
end
|
94
|
+
|
95
|
+
def argument_from_column(column)
|
96
|
+
name = column.name
|
97
|
+
return if argument_exists?(name)
|
98
|
+
argument name, ::HQ::GraphQL::Types.type_from_column(column), required: false
|
99
|
+
end
|
100
|
+
|
101
|
+
def argument_exists?(name)
|
102
|
+
!!arguments[camelize(name)]
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
::GraphQL::Schema::InputObject.include ::HQ::GraphQL::Ext::InputObjectExtensions
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module HQ
|
4
|
+
module GraphQL
|
5
|
+
module Ext
|
6
|
+
module MutationExtensions
|
7
|
+
def self.included(klass)
|
8
|
+
klass.include Scalars
|
9
|
+
klass.include ActiveRecordExtensions
|
10
|
+
klass.singleton_class.prepend PrependMethods
|
11
|
+
end
|
12
|
+
|
13
|
+
module PrependMethods
|
14
|
+
def generate_payload_type
|
15
|
+
lazy_load!
|
16
|
+
super
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
::GraphQL::Schema::Mutation.include ::HQ::GraphQL::Ext::MutationExtensions
|
@@ -0,0 +1,122 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "hq/graphql/ext/active_record_extensions"
|
4
|
+
require "hq/graphql/field"
|
5
|
+
require "hq/graphql/field_extension/association_loader_extension"
|
6
|
+
require "hq/graphql/field_extension/paginated_arguments"
|
7
|
+
require "hq/graphql/field_extension/paginated_loader"
|
8
|
+
require "hq/graphql/object_association"
|
9
|
+
require "hq/graphql/types"
|
10
|
+
|
11
|
+
module HQ
|
12
|
+
module GraphQL
|
13
|
+
module Ext
|
14
|
+
module ObjectExtensions
|
15
|
+
def self.included(klass)
|
16
|
+
klass.include Scalars
|
17
|
+
klass.include ActiveRecordExtensions
|
18
|
+
klass.extend ObjectAssociation
|
19
|
+
klass.singleton_class.prepend PrependMethods
|
20
|
+
klass.field_class Field
|
21
|
+
end
|
22
|
+
|
23
|
+
module PrependMethods
|
24
|
+
def authorize_action(action)
|
25
|
+
self.authorized_action = action
|
26
|
+
end
|
27
|
+
|
28
|
+
def authorized?(object, context)
|
29
|
+
super && ::HQ::GraphQL.authorized?(authorized_action, object, context)
|
30
|
+
end
|
31
|
+
|
32
|
+
def with_model(model_name, attributes: true, associations: true, auto_nil: true, enums: true)
|
33
|
+
self.model_name = model_name
|
34
|
+
self.auto_load_attributes = attributes
|
35
|
+
self.auto_load_associations = associations
|
36
|
+
self.auto_load_enums = enums
|
37
|
+
|
38
|
+
lazy_load do
|
39
|
+
model_columns.each do |column|
|
40
|
+
field_from_column(column, auto_nil: auto_nil)
|
41
|
+
end
|
42
|
+
|
43
|
+
model_associations.each do |association|
|
44
|
+
next if resource_reflections[association.name.to_s]
|
45
|
+
field_from_association(association, auto_nil: auto_nil)
|
46
|
+
end
|
47
|
+
|
48
|
+
resource_reflections.values.each do |resource_reflection|
|
49
|
+
reflection = resource_reflection.reflection(model_klass)
|
50
|
+
next unless reflection
|
51
|
+
field_from_association(reflection, auto_nil: auto_nil, internal_association: true, &resource_reflection.block)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
attr_writer :authorized_action
|
58
|
+
|
59
|
+
def authorized_action
|
60
|
+
@authorized_action ||= :read
|
61
|
+
end
|
62
|
+
|
63
|
+
def field_from_association(association, auto_nil:, internal_association: false, &block)
|
64
|
+
association_klass = association.klass
|
65
|
+
name = association.name.to_s
|
66
|
+
return if field_exists?(name)
|
67
|
+
|
68
|
+
klass = model_klass
|
69
|
+
type = Types[association_klass]
|
70
|
+
|
71
|
+
case association.macro
|
72
|
+
when :has_many
|
73
|
+
field name, [type], null: false, klass: model_name do
|
74
|
+
if ::HQ::GraphQL.use_experimental_associations?
|
75
|
+
extension FieldExtension::PaginatedArguments, klass: association_klass
|
76
|
+
extension FieldExtension::PaginatedLoader, klass: klass, association: name, internal_association: internal_association
|
77
|
+
else
|
78
|
+
extension FieldExtension::AssociationLoaderExtension, klass: klass
|
79
|
+
end
|
80
|
+
instance_eval(&block) if block
|
81
|
+
end
|
82
|
+
when :has_one
|
83
|
+
field name, type, null: !auto_nil || !has_presence_validation?(association), klass: model_name do
|
84
|
+
extension FieldExtension::AssociationLoaderExtension, klass: klass
|
85
|
+
end
|
86
|
+
else
|
87
|
+
field name, type, null: !auto_nil || !association_required?(association), klass: model_name do
|
88
|
+
extension FieldExtension::AssociationLoaderExtension, klass: klass
|
89
|
+
end
|
90
|
+
end
|
91
|
+
rescue Types::Error
|
92
|
+
nil
|
93
|
+
end
|
94
|
+
|
95
|
+
def field_from_column(column, auto_nil:)
|
96
|
+
name = column.name
|
97
|
+
return if field_exists?(name)
|
98
|
+
|
99
|
+
field name, Types.type_from_column(column), null: !auto_nil || column.null
|
100
|
+
end
|
101
|
+
|
102
|
+
def field_exists?(name)
|
103
|
+
!!fields[camelize(name)]
|
104
|
+
end
|
105
|
+
|
106
|
+
def association_required?(association)
|
107
|
+
!association.options[:optional] || has_presence_validation?(association)
|
108
|
+
end
|
109
|
+
|
110
|
+
def has_presence_validation?(association)
|
111
|
+
model_klass.validators.any? do |validation|
|
112
|
+
next unless validation.class == ActiveRecord::Validations::PresenceValidator && !(validation.options.include?(:if) || validation.options.include?(:unless))
|
113
|
+
validation.attributes.any? { |a| a.to_s == association.name.to_s }
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
::GraphQL::Schema::Object.include ::HQ::GraphQL::Ext::ObjectExtensions
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module HQ
|
4
|
+
module GraphQL
|
5
|
+
module Ext
|
6
|
+
module SchemaExtensions
|
7
|
+
def self.prepended(klass)
|
8
|
+
klass.alias_method :add_type_without_lazyload, :add_type
|
9
|
+
klass.alias_method :add_type, :add_type_with_lazyload
|
10
|
+
end
|
11
|
+
|
12
|
+
def execute(*args, **options)
|
13
|
+
load_types!
|
14
|
+
super
|
15
|
+
end
|
16
|
+
|
17
|
+
def dump_directory(directory = Rails.root.join("app/graphql"))
|
18
|
+
@dump_directory ||= directory
|
19
|
+
end
|
20
|
+
|
21
|
+
def dump_filename(filename = "#{self.name.underscore}.graphql")
|
22
|
+
@dump_filename ||= filename
|
23
|
+
end
|
24
|
+
|
25
|
+
def dump
|
26
|
+
load_types!
|
27
|
+
::FileUtils.mkdir_p(dump_directory)
|
28
|
+
::File.open(::File.join(dump_directory, dump_filename), "w") { |file| file.write(self.to_definition) }
|
29
|
+
end
|
30
|
+
|
31
|
+
def load_types!
|
32
|
+
::HQ::GraphQL.load_types!
|
33
|
+
return if @add_type_with_lazyload.blank?
|
34
|
+
while (args, options = @add_type_with_lazyload.shift)
|
35
|
+
add_type_without_lazyload(*args, **options)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# Defer adding types until first schema execution
|
40
|
+
# https://github.com/rmosolgo/graphql-ruby/blob/792f276444e1dd6004fcafe3820d65fdbbe285f0/lib/graphql/schema.rb#L1888-L1980
|
41
|
+
def add_type_with_lazyload(*args, **options)
|
42
|
+
@add_type_with_lazyload ||= []
|
43
|
+
@add_type_with_lazyload.push([args, options])
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
::GraphQL::Schema.singleton_class.prepend ::HQ::GraphQL::Ext::SchemaExtensions
|
data/lib/hq/graphql/field.rb
CHANGED
@@ -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,9 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "hq/graphql/ext/input_object_extensions"
|
4
|
+
require "hq/graphql/ext/object_extensions"
|
3
5
|
require "hq/graphql/enum/sort_by"
|
4
6
|
require "hq/graphql/field_extension/paginated_arguments"
|
5
|
-
require "hq/graphql/input_object"
|
6
|
-
require "hq/graphql/object"
|
7
7
|
require "hq/graphql/resource/auto_mutation"
|
8
8
|
require "hq/graphql/scalars"
|
9
9
|
|
@@ -60,18 +60,21 @@ module HQ
|
|
60
60
|
end
|
61
61
|
|
62
62
|
def nil_query_object
|
63
|
-
@nil_query_object ||= build_graphql_object(name: "#{graphql_name}Copy", auto_nil: false)
|
63
|
+
@nil_query_object ||= const_set(:NilQuery, build_graphql_object(name: "#{graphql_name}Copy", auto_nil: false))
|
64
64
|
end
|
65
65
|
|
66
66
|
def query_object
|
67
67
|
@query_object ||= begin
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
68
|
+
qo =
|
69
|
+
if @query_object_options
|
70
|
+
options, block = @query_object_options
|
71
|
+
@query_object_options = nil
|
72
|
+
build_graphql_object(**options, &block)
|
73
|
+
else
|
74
|
+
build_graphql_object
|
75
|
+
end
|
76
|
+
remove_const(:Query) if const_defined?(:Query, false)
|
77
|
+
const_set(:Query, qo)
|
75
78
|
end
|
76
79
|
end
|
77
80
|
|
@@ -79,6 +82,20 @@ module HQ
|
|
79
82
|
@sort_fields_enum || ::HQ::GraphQL::Enum::SortBy
|
80
83
|
end
|
81
84
|
|
85
|
+
def const_missing(constant_name)
|
86
|
+
constant_name = constant_name.to_sym
|
87
|
+
case constant_name
|
88
|
+
when :Query
|
89
|
+
query_object
|
90
|
+
when :NilQuery
|
91
|
+
nil_query_object
|
92
|
+
when :Input
|
93
|
+
input_klass
|
94
|
+
else
|
95
|
+
super
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
82
99
|
protected
|
83
100
|
|
84
101
|
def default_scope(&block)
|
@@ -115,11 +132,15 @@ module HQ
|
|
115
132
|
def def_root(field_name, is_array: false, null: true, &block)
|
116
133
|
resource = self
|
117
134
|
resolver = -> {
|
118
|
-
Class.new(::GraphQL::Schema::Resolver) do
|
135
|
+
klass = Class.new(::GraphQL::Schema::Resolver) do
|
119
136
|
type = is_array ? [resource.query_object] : resource.query_object
|
120
137
|
type type, null: null
|
121
138
|
class_eval(&block) if block
|
122
139
|
end
|
140
|
+
|
141
|
+
constant_name = "#{field_name.to_s.classify}Resolver"
|
142
|
+
resource.send(:remove_const, constant_name) if resource.const_defined?(constant_name, false)
|
143
|
+
resource.const_set(constant_name, klass)
|
123
144
|
}
|
124
145
|
::HQ::GraphQL.root_queries << {
|
125
146
|
field_name: field_name, resolver: resolver, model_name: model_name
|
@@ -172,7 +193,7 @@ module HQ
|
|
172
193
|
def build_graphql_object(name: graphql_name, **options, &block)
|
173
194
|
scoped_graphql_name = name
|
174
195
|
scoped_model_name = model_name
|
175
|
-
object_class = @query_class || ::HQ::GraphQL.default_object_class || ::
|
196
|
+
object_class = @query_class || ::HQ::GraphQL.default_object_class || ::GraphQL::Schema::Object
|
176
197
|
Class.new(object_class) do
|
177
198
|
graphql_name scoped_graphql_name
|
178
199
|
|
@@ -187,18 +208,22 @@ module HQ
|
|
187
208
|
scoped_model_name = model_name
|
188
209
|
scoped_excluded_inputs = @excluded_inputs || []
|
189
210
|
|
190
|
-
Class.new(::
|
211
|
+
input_klass = Class.new(::GraphQL::Schema::InputObject) do
|
191
212
|
graphql_name "#{scoped_graphql_name}Input"
|
192
213
|
|
193
214
|
with_model scoped_model_name, excluded_inputs: scoped_excluded_inputs, **options
|
194
215
|
|
195
216
|
class_eval(&block) if block
|
196
217
|
end
|
218
|
+
|
219
|
+
remove_const(:Input) if const_defined?(:Input, false)
|
220
|
+
const_set(:Input, input_klass)
|
197
221
|
end
|
198
222
|
|
199
223
|
def sort_fields_enum=(fields)
|
200
224
|
@sort_fields_enum ||= Class.new(::HQ::GraphQL::Enum::SortBy).tap do |c|
|
201
225
|
c.graphql_name "#{graphql_name}Sort"
|
226
|
+
const_set(:Sort, c)
|
202
227
|
end
|
203
228
|
|
204
229
|
Array(fields).each do |field|
|