hq-graphql 2.0.7 → 2.1.1

Sign up to get free protection for your applications and to get access to all the features.
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