hq-graphql 2.0.7 → 2.1.1

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 (36) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +28 -0
  3. data/lib/hq-graphql.rb +0 -2
  4. data/lib/hq/graphql.rb +28 -21
  5. data/lib/hq/graphql/active_record_extensions.rb +23 -27
  6. data/lib/hq/graphql/association_loader.rb +49 -0
  7. data/lib/hq/graphql/comparator.rb +1 -1
  8. data/lib/hq/graphql/config.rb +17 -11
  9. data/lib/hq/graphql/engine.rb +0 -1
  10. data/lib/hq/graphql/enum.rb +77 -0
  11. data/lib/hq/graphql/enum/sort_by.rb +10 -0
  12. data/lib/hq/graphql/enum/sort_order.rb +10 -0
  13. data/lib/hq/graphql/field.rb +12 -11
  14. data/lib/hq/graphql/field_extension/association_loader_extension.rb +15 -0
  15. data/lib/hq/graphql/field_extension/paginated_arguments.rb +22 -0
  16. data/lib/hq/graphql/field_extension/paginated_loader.rb +45 -0
  17. data/lib/hq/graphql/input_object.rb +12 -7
  18. data/lib/hq/graphql/inputs.rb +4 -3
  19. data/lib/hq/graphql/mutation.rb +0 -1
  20. data/lib/hq/graphql/object.rb +42 -11
  21. data/lib/hq/graphql/object_association.rb +50 -0
  22. data/lib/hq/graphql/paginated_association_loader.rb +158 -0
  23. data/lib/hq/graphql/resource.rb +47 -156
  24. data/lib/hq/graphql/resource/auto_mutation.rb +163 -0
  25. data/lib/hq/graphql/root_mutation.rb +1 -2
  26. data/lib/hq/graphql/root_query.rb +0 -1
  27. data/lib/hq/graphql/scalars.rb +0 -1
  28. data/lib/hq/graphql/schema.rb +1 -1
  29. data/lib/hq/graphql/types.rb +22 -8
  30. data/lib/hq/graphql/types/object.rb +7 -11
  31. data/lib/hq/graphql/types/uuid.rb +7 -14
  32. data/lib/hq/graphql/version.rb +1 -2
  33. metadata +12 -39
  34. data/lib/hq/graphql/loaders.rb +0 -4
  35. data/lib/hq/graphql/loaders/association.rb +0 -52
  36. data/lib/hq/graphql/resource/mutation.rb +0 -39
@@ -1,21 +1,26 @@
1
- # typed: false
2
1
  # frozen_string_literal: true
3
2
 
4
- require "hq/graphql/resource/mutation"
3
+ require "hq/graphql/enum/sort_by"
4
+ require "hq/graphql/field_extension/paginated_arguments"
5
+ require "hq/graphql/input_object"
6
+ require "hq/graphql/object"
7
+ require "hq/graphql/resource/auto_mutation"
8
+ require "hq/graphql/scalars"
5
9
 
6
10
  module HQ
7
11
  module GraphQL
8
12
  module Resource
9
- extend T::Helpers
10
-
11
13
  def self.included(base)
12
14
  super
13
- ::HQ::GraphQL.types << base
15
+ ::HQ::GraphQL.resources << base
14
16
  base.include Scalars
15
17
  base.include ::GraphQL::Types
18
+ base.extend ClassMethods
16
19
  end
17
20
 
18
21
  module ClassMethods
22
+ include AutoMutation
23
+
19
24
  attr_writer :graphql_name, :model_name
20
25
 
21
26
  def scope(context)
@@ -39,7 +44,7 @@ module HQ
39
44
  end
40
45
 
41
46
  def model_name
42
- @model_name || name.demodulize
47
+ @model_name || ::HQ::GraphQL.extract_class(self)
43
48
  end
44
49
 
45
50
  def model_klass
@@ -62,6 +67,10 @@ module HQ
62
67
  @query_klass ||= build_graphql_object
63
68
  end
64
69
 
70
+ def sort_fields_enum
71
+ @sort_fields_enum || ::HQ::GraphQL::Enum::SortBy
72
+ end
73
+
65
74
  protected
66
75
 
67
76
  def default_scope(&block)
@@ -73,155 +82,25 @@ module HQ
73
82
  end
74
83
 
75
84
  def mutations(create: true, copy: true, update: true, destroy: true)
76
- scoped_graphql_name = graphql_name
77
- scoped_model_name = model_name
78
- scoped_self = self
79
-
80
- if create
81
- create_mutation = ::HQ::GraphQL::Resource::Mutation.build(model_name, action: :create, graphql_name: "#{scoped_graphql_name}Create") do
82
- define_method(:resolve) do |**args|
83
- resource = scoped_self.new_record(context)
84
- resource.assign_attributes(args[:attributes].format_nested_attributes)
85
- if resource.save
86
- {
87
- resource: resource,
88
- errors: {},
89
- }
90
- else
91
- {
92
- resource: nil,
93
- errors: errors_from_resource(resource)
94
- }
95
- end
96
- end
97
-
98
- lazy_load do
99
- argument :attributes, ::HQ::GraphQL::Inputs[scoped_model_name], required: true
100
- end
101
- end
102
-
103
- mutation_klasses["create_#{scoped_graphql_name.underscore}"] = create_mutation
104
- end
105
-
106
- if copy
107
- copy_mutation = ::HQ::GraphQL::Resource::Mutation.build(
108
- model_name,
109
- action: :copy,
110
- graphql_name: "#{scoped_graphql_name}Copy",
111
- require_primary_key: true,
112
- nil_klass: true
113
- ) do
114
- define_method(:resolve) do |**args|
115
- resource = scoped_self.find_record(args, context)
116
-
117
- if resource
118
- copy = resource.copy
119
- if copy.save
120
- {
121
- resource: copy,
122
- errors: {},
123
- }
124
- else
125
- {
126
- resource: copy,
127
- errors: errors_from_resource(copy)
128
- }
129
- end
130
- else
131
- {
132
- resource: nil,
133
- errors: { resource: "Unable to find #{scoped_graphql_name}" }
134
- }
135
- end
136
- end
137
- end
138
-
139
- mutation_klasses["copy_#{scoped_graphql_name.underscore}"] = copy_mutation
140
- end
141
-
142
- if update
143
- update_mutation = ::HQ::GraphQL::Resource::Mutation.build(
144
- model_name,
145
- action: :update,
146
- graphql_name: "#{scoped_graphql_name}Update",
147
- require_primary_key: true
148
- ) do
149
- define_method(:resolve) do |**args|
150
- resource = scoped_self.find_record(args, context)
151
-
152
- if resource
153
- resource.assign_attributes(args[:attributes].format_nested_attributes)
154
- if resource.save
155
- {
156
- resource: resource,
157
- errors: {},
158
- }
159
- else
160
- {
161
- resource: nil,
162
- errors: errors_from_resource(resource)
163
- }
164
- end
165
- else
166
- {
167
- resource: nil,
168
- errors: { resource: "Unable to find #{scoped_graphql_name}" }
169
- }
170
- end
171
- end
172
-
173
- lazy_load do
174
- argument :attributes, ::HQ::GraphQL::Inputs[scoped_model_name], required: true
175
- end
176
- end
177
-
178
- mutation_klasses["update_#{scoped_graphql_name.underscore}"] = update_mutation
179
- end
180
-
181
- if destroy
182
- destroy_mutation = ::HQ::GraphQL::Resource::Mutation.build(
183
- model_name,
184
- action: :destroy,
185
- graphql_name: "#{scoped_graphql_name}Destroy",
186
- require_primary_key: true
187
- ) do
188
- define_method(:resolve) do |**attrs|
189
- resource = scoped_self.find_record(attrs, context)
190
-
191
- if resource
192
- if resource.destroy
193
- {
194
- resource: resource,
195
- errors: {},
196
- }
197
- else
198
- {
199
- resource: nil,
200
- errors: errors_from_resource(resource)
201
- }
202
- end
203
- else
204
- {
205
- resource: nil,
206
- errors: { resource: "Unable to find #{scoped_graphql_name}" }
207
- }
208
- end
209
- end
210
- end
211
-
212
- mutation_klasses["destroy_#{scoped_graphql_name.underscore}"] = destroy_mutation
213
- end
85
+ mutation_klasses["create_#{graphql_name.underscore}"] = build_create if create
86
+ mutation_klasses["copy_#{graphql_name.underscore}"] = build_copy if copy
87
+ mutation_klasses["update_#{graphql_name.underscore}"] = build_update if update
88
+ mutation_klasses["destroy_#{graphql_name.underscore}"] = build_destroy if destroy
214
89
  end
215
90
 
216
91
  def query(**options, &block)
217
92
  @query_klass = build_graphql_object(**options, &block)
218
93
  end
219
94
 
95
+ def sort_fields(*fields)
96
+ self.sort_fields_enum = fields
97
+ end
98
+
220
99
  def def_root(field_name, is_array: false, null: true, &block)
221
- graphql = self
100
+ resource = self
222
101
  resolver = -> {
223
102
  Class.new(::GraphQL::Schema::Resolver) do
224
- type = is_array ? [graphql.query_klass] : graphql.query_klass
103
+ type = is_array ? [resource.query_klass] : resource.query_klass
225
104
  type type, null: null
226
105
  class_eval(&block) if block
227
106
  end
@@ -231,7 +110,7 @@ module HQ
231
110
  }
232
111
  end
233
112
 
234
- def root_query(find_one: true, find_all: true, pagination: false, per_page_max: 250)
113
+ def root_query(find_one: true, find_all: true, pagination: true, limit_max: 250)
235
114
  field_name = graphql_name.underscore
236
115
  scoped_self = self
237
116
 
@@ -250,18 +129,22 @@ module HQ
250
129
 
251
130
  if find_all
252
131
  def_root field_name.pluralize, is_array: true, null: false do
253
- argument :page, Integer, required: false
254
- argument :per_page, Integer, required: false
132
+ extension FieldExtension::PaginatedArguments, klass: scoped_self.model_klass if pagination
255
133
 
256
- define_method(:resolve) do |page: nil, per_page: nil, **_attrs|
134
+ define_method(:resolve) do |limit: nil, offset: nil, sort_by: nil, sort_order: nil, **_attrs|
257
135
  scope = scoped_self.scope(context).all
258
136
 
259
- if pagination || page || per_page
260
- page ||= 0
261
- limit = [per_page_max, *per_page].min
262
- scope = scope.limit(limit).offset(page * limit)
137
+ if pagination || page || limit
138
+ offset = [0, *offset].max
139
+ limit = [[limit_max, *limit].min, 0].max
140
+ scope = scope.limit(limit).offset(offset)
263
141
  end
264
142
 
143
+ sort_by ||= :updated_at
144
+ sort_order ||= :desc
145
+ # There should be no risk for SQL injection since an enum is being used for both sort_by and sort_order
146
+ scope = scope.reorder(sort_by => sort_order)
147
+
265
148
  scope
266
149
  end
267
150
  end
@@ -293,9 +176,17 @@ module HQ
293
176
  class_eval(&block) if block
294
177
  end
295
178
  end
296
- end
297
179
 
298
- mixes_in_class_methods(ClassMethods)
180
+ def sort_fields_enum=(fields)
181
+ @sort_fields_enum ||= Class.new(::HQ::GraphQL::Enum::SortBy).tap do |c|
182
+ c.graphql_name "#{graphql_name}Sort"
183
+ end
184
+
185
+ Array(fields).each do |field|
186
+ @sort_fields_enum.value field.to_s.classify, value: field
187
+ end
188
+ end
189
+ end
299
190
  end
300
191
  end
301
192
  end
@@ -0,0 +1,163 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "hq/graphql/inputs"
4
+ require "hq/graphql/mutation"
5
+ require "hq/graphql/types"
6
+
7
+ module HQ
8
+ module GraphQL
9
+ module Resource
10
+ module AutoMutation
11
+ def build_create
12
+ scoped_self = self
13
+
14
+ build_mutation(action: :create) do
15
+ define_method(:resolve) do |**args|
16
+ resource = scoped_self.new_record(context)
17
+ resource.assign_attributes(args[:attributes].format_nested_attributes)
18
+ if resource.save
19
+ {
20
+ resource: resource,
21
+ errors: {},
22
+ }
23
+ else
24
+ {
25
+ resource: nil,
26
+ errors: errors_from_resource(resource)
27
+ }
28
+ end
29
+ end
30
+
31
+ lazy_load do
32
+ argument :attributes, ::HQ::GraphQL::Inputs[scoped_self.model_name], required: true
33
+ end
34
+ end
35
+ end
36
+
37
+ def build_update
38
+ scoped_self = self
39
+
40
+ build_mutation(action: :update, require_primary_key: true) do
41
+ define_method(:resolve) do |**args|
42
+ resource = scoped_self.find_record(args, context)
43
+
44
+ if resource
45
+ resource.assign_attributes(args[:attributes].format_nested_attributes)
46
+ if resource.save
47
+ {
48
+ resource: resource,
49
+ errors: {},
50
+ }
51
+ else
52
+ {
53
+ resource: nil,
54
+ errors: errors_from_resource(resource)
55
+ }
56
+ end
57
+ else
58
+ {
59
+ resource: nil,
60
+ errors: { resource: "Unable to find #{self.class.graphql_name}" }
61
+ }
62
+ end
63
+ end
64
+
65
+ lazy_load do
66
+ argument :attributes, ::HQ::GraphQL::Inputs[scoped_self.model_name], required: true
67
+ end
68
+ end
69
+ end
70
+
71
+ def build_copy
72
+ scoped_self = self
73
+
74
+ build_mutation(action: :copy, require_primary_key: true, nil_klass: true) do
75
+ define_method(:resolve) do |**args|
76
+ resource = scoped_self.find_record(args, context)
77
+
78
+ if resource
79
+ copy = resource.copy
80
+ if copy.save
81
+ {
82
+ resource: copy,
83
+ errors: {},
84
+ }
85
+ else
86
+ {
87
+ resource: copy,
88
+ errors: errors_from_resource(copy)
89
+ }
90
+ end
91
+ else
92
+ {
93
+ resource: nil,
94
+ errors: { resource: "Unable to find #{self.class.graphql_name}" }
95
+ }
96
+ end
97
+ end
98
+ end
99
+ end
100
+
101
+ def build_destroy
102
+ scoped_self = self
103
+
104
+ build_mutation(action: :destroy, require_primary_key: true) do
105
+ define_method(:resolve) do |**attrs|
106
+ resource = scoped_self.find_record(attrs, context)
107
+
108
+ if resource
109
+ if resource.destroy
110
+ {
111
+ resource: resource,
112
+ errors: {},
113
+ }
114
+ else
115
+ {
116
+ resource: nil,
117
+ errors: errors_from_resource(resource)
118
+ }
119
+ end
120
+ else
121
+ {
122
+ resource: nil,
123
+ errors: { resource: "Unable to find #{self.class.graphql_name}" }
124
+ }
125
+ end
126
+ end
127
+ end
128
+ end
129
+
130
+ def build_mutation(action:, require_primary_key: false, nil_klass: false, &block)
131
+ gql_name = "#{graphql_name}#{action.to_s.titleize}"
132
+ scoped_model_name = model_name
133
+ Class.new(::HQ::GraphQL::Mutation) do
134
+ graphql_name gql_name
135
+
136
+ define_method(:ready?) do |*args|
137
+ super(*args) && ::HQ::GraphQL.authorized?(action, scoped_model_name, context)
138
+ end
139
+
140
+ lazy_load do
141
+ field :errors, ::HQ::GraphQL::Types::Object, null: false
142
+ field :resource, ::HQ::GraphQL::Types[scoped_model_name, nil_klass], null: true
143
+ end
144
+
145
+ instance_eval(&block)
146
+
147
+ if require_primary_key
148
+ lazy_load do
149
+ klass = scoped_model_name.constantize
150
+ primary_key = klass.primary_key
151
+ argument primary_key, ::GraphQL::Types::ID, required: true
152
+ end
153
+ end
154
+
155
+ def errors_from_resource(resource)
156
+ resource.errors.to_h.deep_transform_keys { |k| k.to_s.camelize(:lower) }
157
+ end
158
+ end
159
+ end
160
+ end
161
+ end
162
+ end
163
+ end
@@ -1,4 +1,3 @@
1
- # typed: true
2
1
  # frozen_string_literal: true
3
2
 
4
3
  module HQ
@@ -8,7 +7,7 @@ module HQ
8
7
  super
9
8
  base.class_eval do
10
9
  lazy_load do
11
- ::HQ::GraphQL.types.each do |type|
10
+ ::HQ::GraphQL.resources.each do |type|
12
11
  type.mutation_klasses.each do |mutation_name, klass|
13
12
  field mutation_name, mutation: klass
14
13
  end