hq-graphql 2.0.11 → 2.1.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/lib/hq/graphql.rb +12 -5
- data/lib/hq/graphql/config.rb +1 -0
- data/lib/hq/graphql/enum.rb +3 -0
- data/lib/hq/graphql/enum/sort_by.rb +8 -0
- data/lib/hq/graphql/enum/sort_order.rb +8 -0
- data/lib/hq/graphql/field.rb +26 -5
- data/lib/hq/graphql/inputs.rb +3 -7
- data/lib/hq/graphql/object.rb +13 -2
- data/lib/hq/graphql/paginated_association_loader.rb +138 -0
- data/lib/hq/graphql/resource.rb +45 -150
- data/lib/hq/graphql/resource/auto_mutation.rb +159 -0
- data/lib/hq/graphql/root_mutation.rb +1 -1
- data/lib/hq/graphql/types.rb +3 -9
- data/lib/hq/graphql/version.rb +1 -1
- metadata +6 -3
- data/lib/hq/graphql/resource/mutation.rb +0 -38
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 05c40fc2e6eaccc1277c1b6f36b19983af507721b011d6d25c9010f877c18db7
|
4
|
+
data.tar.gz: cf67ac6f3d02f3c6a5c5b0f1124e8aacd706d1e30b0077af69cad336d5dc1475
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2f643af54cb80d2325dc8829c620f9c4d0c48a950aced92cb0313b2aacba52a3b797f42deb0c1f8848b9454cf70d014c4795f64d5e34444d87cc4fdbe36702c8
|
7
|
+
data.tar.gz: df55f4034030bed0f035a170d581bfe030f62a2545b2f4b7d15d48838c9777a7e4aa357e5f86618ee7bb692ba001c263df9973f8e546f8a1a902d0e8f61d4821
|
data/lib/hq/graphql.rb
CHANGED
@@ -32,14 +32,20 @@ module HQ
|
|
32
32
|
config.extract_class.call(klass)
|
33
33
|
end
|
34
34
|
|
35
|
-
def self.
|
36
|
-
|
35
|
+
def self.lookup_resource(klass)
|
36
|
+
[klass, klass.base_class, klass.superclass].lazy.map do |k|
|
37
|
+
config.resource_lookup.call(k) || resources.detect { |r| r.model_klass == k }
|
38
|
+
end.reject(&:nil?).first
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.use_experimental_associations?
|
42
|
+
!!config.use_experimental_associations
|
37
43
|
end
|
38
44
|
|
39
45
|
def self.reset!
|
40
46
|
@root_queries = nil
|
41
47
|
@enums = nil
|
42
|
-
@
|
48
|
+
@resources = nil
|
43
49
|
::HQ::GraphQL::Inputs.reset!
|
44
50
|
::HQ::GraphQL::Types.reset!
|
45
51
|
end
|
@@ -52,8 +58,8 @@ module HQ
|
|
52
58
|
@enums ||= Set.new
|
53
59
|
end
|
54
60
|
|
55
|
-
def self.
|
56
|
-
@
|
61
|
+
def self.resources
|
62
|
+
@resources ||= Set.new
|
57
63
|
end
|
58
64
|
end
|
59
65
|
end
|
@@ -67,6 +73,7 @@ require "hq/graphql/inputs"
|
|
67
73
|
require "hq/graphql/input_object"
|
68
74
|
require "hq/graphql/mutation"
|
69
75
|
require "hq/graphql/object"
|
76
|
+
require "hq/graphql/paginated_association_loader"
|
70
77
|
require "hq/graphql/resource"
|
71
78
|
require "hq/graphql/root_mutation"
|
72
79
|
require "hq/graphql/root_query"
|
data/lib/hq/graphql/config.rb
CHANGED
data/lib/hq/graphql/enum.rb
CHANGED
data/lib/hq/graphql/field.rb
CHANGED
@@ -3,13 +3,13 @@
|
|
3
3
|
module HQ
|
4
4
|
module GraphQL
|
5
5
|
class Field < ::GraphQL::Schema::Field
|
6
|
-
attr_reader :authorize_action, :authorize
|
6
|
+
attr_reader :authorize_action, :authorize
|
7
7
|
|
8
8
|
def initialize(*args, authorize_action: :read, authorize: nil, klass: nil, **options, &block)
|
9
9
|
super(*args, **options, &block)
|
10
10
|
@authorize_action = authorize_action
|
11
11
|
@authorize = authorize
|
12
|
-
@
|
12
|
+
@class_name = klass
|
13
13
|
end
|
14
14
|
|
15
15
|
def authorized?(object, ctx)
|
@@ -20,13 +20,34 @@ module HQ
|
|
20
20
|
|
21
21
|
def resolve_field(object, args, ctx)
|
22
22
|
if klass.present? && !!::GraphQL::Batch::Executor.current && object.object
|
23
|
-
|
24
|
-
|
25
|
-
|
23
|
+
loader =
|
24
|
+
if ::HQ::GraphQL.use_experimental_associations?
|
25
|
+
limit = args[:limit]
|
26
|
+
offset = args[:offset]
|
27
|
+
sort_by = args[:sortBy]
|
28
|
+
sort_order = args[:sortOrder]
|
29
|
+
|
30
|
+
PaginatedAssociationLoader.for(
|
31
|
+
klass,
|
32
|
+
original_name,
|
33
|
+
limit: limit,
|
34
|
+
offset: offset,
|
35
|
+
sort_by: sort_by,
|
36
|
+
sort_order: sort_order
|
37
|
+
)
|
38
|
+
else
|
39
|
+
AssociationLoader.for(klass, original_name)
|
40
|
+
end
|
41
|
+
|
42
|
+
loader.load(object.object)
|
26
43
|
else
|
27
44
|
super
|
28
45
|
end
|
29
46
|
end
|
47
|
+
|
48
|
+
def klass
|
49
|
+
@klass ||= @class_name&.constantize
|
50
|
+
end
|
30
51
|
end
|
31
52
|
end
|
32
53
|
end
|
data/lib/hq/graphql/inputs.rb
CHANGED
@@ -24,14 +24,10 @@ module HQ
|
|
24
24
|
|
25
25
|
def klass_for(klass_or_string)
|
26
26
|
klass = klass_or_string.is_a?(String) ? klass_or_string.constantize : klass_or_string
|
27
|
-
|
27
|
+
resource = ::HQ::GraphQL.lookup_resource(klass)
|
28
28
|
|
29
|
-
raise(Error, Error::MISSING_TYPE_MSG % { klass: klass.name }) if !
|
30
|
-
|
31
|
-
end
|
32
|
-
|
33
|
-
def find_type(klass)
|
34
|
-
::HQ::GraphQL.resource_lookup(klass) || ::HQ::GraphQL.types.detect { |t| t.model_klass == klass }
|
29
|
+
raise(Error, Error::MISSING_TYPE_MSG % { klass: klass.name }) if !resource
|
30
|
+
resource.input_klass
|
35
31
|
end
|
36
32
|
end
|
37
33
|
end
|
data/lib/hq/graphql/object.rb
CHANGED
@@ -47,11 +47,22 @@ module HQ
|
|
47
47
|
end
|
48
48
|
|
49
49
|
def field_from_association(association, auto_nil:)
|
50
|
-
|
50
|
+
# The PaginationAssociationLoader doesn't support through associations yet
|
51
|
+
return if association.through_reflection? && ::HQ::GraphQL.use_experimental_associations?
|
52
|
+
|
53
|
+
association_klass = association.klass
|
54
|
+
type = ::HQ::GraphQL::Types[association_klass]
|
51
55
|
name = association.name
|
52
56
|
case association.macro
|
53
57
|
when :has_many
|
54
|
-
field name, [type], null: false, klass: model_name
|
58
|
+
field name, [type], null: false, klass: model_name do
|
59
|
+
if ::HQ::GraphQL.use_experimental_associations? && (resource = ::HQ::GraphQL.lookup_resource(association_klass))
|
60
|
+
argument :offset, Integer, required: false
|
61
|
+
argument :limit, Integer, required: false
|
62
|
+
argument :sort_by, resource.sort_fields_enum, required: false
|
63
|
+
argument :sort_order, Enum::SortOrder, required: false
|
64
|
+
end
|
65
|
+
end
|
55
66
|
else
|
56
67
|
field name, type, null: !auto_nil || !association_required?(association), klass: model_name
|
57
68
|
end
|
@@ -0,0 +1,138 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module HQ
|
4
|
+
module GraphQL
|
5
|
+
class PaginatedAssociationLoader < ::GraphQL::Batch::Loader
|
6
|
+
def initialize(model, association_name, limit: nil, offset: nil, sort_by: nil, sort_order: nil)
|
7
|
+
@model = model
|
8
|
+
@association_name = association_name
|
9
|
+
@limit = [0, limit].max if limit
|
10
|
+
@offset = [0, offset].max if offset
|
11
|
+
@sort_by = sort_by || :updated_at
|
12
|
+
@sort_order = normalize_sort_order(sort_order)
|
13
|
+
|
14
|
+
validate!
|
15
|
+
end
|
16
|
+
|
17
|
+
def load(record)
|
18
|
+
raise TypeError, "#{@model} loader can't load association for #{record.class}" unless record.is_a?(@model)
|
19
|
+
super
|
20
|
+
end
|
21
|
+
|
22
|
+
def cache_key(record)
|
23
|
+
record.send(primary_key)
|
24
|
+
end
|
25
|
+
|
26
|
+
def perform(records)
|
27
|
+
scope =
|
28
|
+
if @limit || @offset
|
29
|
+
# If a limit or offset is added, then we need to transform the query
|
30
|
+
# into a lateral join so that we can limit on groups of data.
|
31
|
+
#
|
32
|
+
# > SELECT * FROM addresses WHERE addresses.user_id IN ($1, $2, ..., $N) ORDER BY addresses.created_at DESC;
|
33
|
+
# ...becomes
|
34
|
+
# > SELECT DISTINCT a_top.*
|
35
|
+
# > FROM addresses
|
36
|
+
# > INNER JOIN LATERAL (
|
37
|
+
# > SELECT inner.*
|
38
|
+
# > FROM addresses inner
|
39
|
+
# > WHERE inner.user_id = addresses.user_id
|
40
|
+
# > ORDER BY inner.created_at DESC
|
41
|
+
# > LIMIT 1
|
42
|
+
# > ) a_top ON TRUE
|
43
|
+
# > WHERE addresses.user_id IN ($1, $2, ..., $N)
|
44
|
+
# > ORDER BY a_top.created_at DESC
|
45
|
+
inner_table = association_class.arel_table
|
46
|
+
association_table = inner_table.alias("outer")
|
47
|
+
|
48
|
+
inside_scope = default_scope.
|
49
|
+
select(inner_table[::Arel.star]).
|
50
|
+
from(inner_table).
|
51
|
+
where(inner_table[association_key].eq(association_table[association_key])).
|
52
|
+
reorder(arel_order(inner_table)).
|
53
|
+
limit(@limit).
|
54
|
+
offset(@offset)
|
55
|
+
|
56
|
+
outside_table = ::Arel::Table.new("top")
|
57
|
+
association_class.
|
58
|
+
select(outside_table[::Arel.star]).distinct.
|
59
|
+
from(association_table).
|
60
|
+
joins("INNER JOIN LATERAL (#{inside_scope.to_sql}) #{outside_table.name} ON TRUE").
|
61
|
+
where(association_table[association_key].in(records.map { |r| join_value(r) })).
|
62
|
+
reorder(arel_order(outside_table))
|
63
|
+
else
|
64
|
+
default_scope.
|
65
|
+
reorder(arel_order(association_class.arel_table)).
|
66
|
+
where(association_key => records.map { |r| join_value(r) })
|
67
|
+
end
|
68
|
+
|
69
|
+
results = scope.to_a
|
70
|
+
records.each do |record|
|
71
|
+
fulfill(record, association_value(record, results)) unless fulfilled?(record)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
private
|
76
|
+
|
77
|
+
def association_key
|
78
|
+
belongs_to? ? association.association_primary_key : association.foreign_key
|
79
|
+
end
|
80
|
+
|
81
|
+
def association_value(record, results)
|
82
|
+
enumerator = has_many? ? :select : :detect
|
83
|
+
results.send(enumerator) { |r| r.send(association_key) == join_value(record) }
|
84
|
+
end
|
85
|
+
|
86
|
+
def join_key
|
87
|
+
belongs_to? ? association.foreign_key : association.association_primary_key
|
88
|
+
end
|
89
|
+
|
90
|
+
def join_value(record)
|
91
|
+
record.send(join_key)
|
92
|
+
end
|
93
|
+
|
94
|
+
def default_scope
|
95
|
+
scope = association_class
|
96
|
+
scope = association.scopes.reduce(scope, &:merge)
|
97
|
+
scope = association_class.default_scopes.reduce(scope, &:merge)
|
98
|
+
scope
|
99
|
+
end
|
100
|
+
|
101
|
+
def belongs_to?
|
102
|
+
association.macro == :belongs_to
|
103
|
+
end
|
104
|
+
|
105
|
+
def has_many?
|
106
|
+
association.macro == :has_many
|
107
|
+
end
|
108
|
+
|
109
|
+
def association
|
110
|
+
@model.reflect_on_association(@association_name)
|
111
|
+
end
|
112
|
+
|
113
|
+
def association_class
|
114
|
+
association.klass
|
115
|
+
end
|
116
|
+
|
117
|
+
def primary_key
|
118
|
+
@model.primary_key
|
119
|
+
end
|
120
|
+
|
121
|
+
def arel_order(table)
|
122
|
+
table[@sort_by].send(@sort_order)
|
123
|
+
end
|
124
|
+
|
125
|
+
def normalize_sort_order(input)
|
126
|
+
if input.to_s.casecmp("asc").zero?
|
127
|
+
:asc
|
128
|
+
else
|
129
|
+
:desc
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
def validate!
|
134
|
+
raise ArgumentError, "No association #{@association_name} on #{@model}" unless association
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
data/lib/hq/graphql/resource.rb
CHANGED
@@ -1,21 +1,41 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "hq/graphql/resource/
|
3
|
+
require "hq/graphql/resource/auto_mutation"
|
4
4
|
|
5
5
|
module HQ
|
6
6
|
module GraphQL
|
7
7
|
module Resource
|
8
8
|
def self.included(base)
|
9
9
|
super
|
10
|
-
::HQ::GraphQL.
|
10
|
+
::HQ::GraphQL.resources << base
|
11
11
|
base.include Scalars
|
12
12
|
base.include ::GraphQL::Types
|
13
13
|
base.extend ClassMethods
|
14
14
|
end
|
15
15
|
|
16
16
|
module ClassMethods
|
17
|
+
include AutoMutation
|
18
|
+
|
17
19
|
attr_writer :graphql_name, :model_name
|
18
20
|
|
21
|
+
def sort_fields(*fields)
|
22
|
+
self.sort_fields_enum = fields
|
23
|
+
end
|
24
|
+
|
25
|
+
def sort_fields_enum
|
26
|
+
@sort_fields_enum || ::HQ::GraphQL::Enum::SortBy
|
27
|
+
end
|
28
|
+
|
29
|
+
def sort_fields_enum=(fields)
|
30
|
+
@sort_fields_enum ||= Class.new(::HQ::GraphQL::Enum::SortBy).tap do |c|
|
31
|
+
c.graphql_name "#{graphql_name}Sort"
|
32
|
+
end
|
33
|
+
|
34
|
+
Array(fields).each do |field|
|
35
|
+
@sort_fields_enum.value field.to_s.classify, value: field
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
19
39
|
def scope(context)
|
20
40
|
scope = model_klass
|
21
41
|
scope = ::HQ::GraphQL.default_scope(scope, context)
|
@@ -71,144 +91,10 @@ module HQ
|
|
71
91
|
end
|
72
92
|
|
73
93
|
def mutations(create: true, copy: true, update: true, destroy: true)
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
if create
|
79
|
-
create_mutation = ::HQ::GraphQL::Resource::Mutation.build(model_name, action: :create, graphql_name: "#{scoped_graphql_name}Create") do
|
80
|
-
define_method(:resolve) do |**args|
|
81
|
-
resource = scoped_self.new_record(context)
|
82
|
-
resource.assign_attributes(args[:attributes].format_nested_attributes)
|
83
|
-
if resource.save
|
84
|
-
{
|
85
|
-
resource: resource,
|
86
|
-
errors: {},
|
87
|
-
}
|
88
|
-
else
|
89
|
-
{
|
90
|
-
resource: nil,
|
91
|
-
errors: errors_from_resource(resource)
|
92
|
-
}
|
93
|
-
end
|
94
|
-
end
|
95
|
-
|
96
|
-
lazy_load do
|
97
|
-
argument :attributes, ::HQ::GraphQL::Inputs[scoped_model_name], required: true
|
98
|
-
end
|
99
|
-
end
|
100
|
-
|
101
|
-
mutation_klasses["create_#{scoped_graphql_name.underscore}"] = create_mutation
|
102
|
-
end
|
103
|
-
|
104
|
-
if copy
|
105
|
-
copy_mutation = ::HQ::GraphQL::Resource::Mutation.build(
|
106
|
-
model_name,
|
107
|
-
action: :copy,
|
108
|
-
graphql_name: "#{scoped_graphql_name}Copy",
|
109
|
-
require_primary_key: true,
|
110
|
-
nil_klass: true
|
111
|
-
) do
|
112
|
-
define_method(:resolve) do |**args|
|
113
|
-
resource = scoped_self.find_record(args, context)
|
114
|
-
|
115
|
-
if resource
|
116
|
-
copy = resource.copy
|
117
|
-
if copy.save
|
118
|
-
{
|
119
|
-
resource: copy,
|
120
|
-
errors: {},
|
121
|
-
}
|
122
|
-
else
|
123
|
-
{
|
124
|
-
resource: copy,
|
125
|
-
errors: errors_from_resource(copy)
|
126
|
-
}
|
127
|
-
end
|
128
|
-
else
|
129
|
-
{
|
130
|
-
resource: nil,
|
131
|
-
errors: { resource: "Unable to find #{scoped_graphql_name}" }
|
132
|
-
}
|
133
|
-
end
|
134
|
-
end
|
135
|
-
end
|
136
|
-
|
137
|
-
mutation_klasses["copy_#{scoped_graphql_name.underscore}"] = copy_mutation
|
138
|
-
end
|
139
|
-
|
140
|
-
if update
|
141
|
-
update_mutation = ::HQ::GraphQL::Resource::Mutation.build(
|
142
|
-
model_name,
|
143
|
-
action: :update,
|
144
|
-
graphql_name: "#{scoped_graphql_name}Update",
|
145
|
-
require_primary_key: true
|
146
|
-
) do
|
147
|
-
define_method(:resolve) do |**args|
|
148
|
-
resource = scoped_self.find_record(args, context)
|
149
|
-
|
150
|
-
if resource
|
151
|
-
resource.assign_attributes(args[:attributes].format_nested_attributes)
|
152
|
-
if resource.save
|
153
|
-
{
|
154
|
-
resource: resource,
|
155
|
-
errors: {},
|
156
|
-
}
|
157
|
-
else
|
158
|
-
{
|
159
|
-
resource: nil,
|
160
|
-
errors: errors_from_resource(resource)
|
161
|
-
}
|
162
|
-
end
|
163
|
-
else
|
164
|
-
{
|
165
|
-
resource: nil,
|
166
|
-
errors: { resource: "Unable to find #{scoped_graphql_name}" }
|
167
|
-
}
|
168
|
-
end
|
169
|
-
end
|
170
|
-
|
171
|
-
lazy_load do
|
172
|
-
argument :attributes, ::HQ::GraphQL::Inputs[scoped_model_name], required: true
|
173
|
-
end
|
174
|
-
end
|
175
|
-
|
176
|
-
mutation_klasses["update_#{scoped_graphql_name.underscore}"] = update_mutation
|
177
|
-
end
|
178
|
-
|
179
|
-
if destroy
|
180
|
-
destroy_mutation = ::HQ::GraphQL::Resource::Mutation.build(
|
181
|
-
model_name,
|
182
|
-
action: :destroy,
|
183
|
-
graphql_name: "#{scoped_graphql_name}Destroy",
|
184
|
-
require_primary_key: true
|
185
|
-
) do
|
186
|
-
define_method(:resolve) do |**attrs|
|
187
|
-
resource = scoped_self.find_record(attrs, context)
|
188
|
-
|
189
|
-
if resource
|
190
|
-
if resource.destroy
|
191
|
-
{
|
192
|
-
resource: resource,
|
193
|
-
errors: {},
|
194
|
-
}
|
195
|
-
else
|
196
|
-
{
|
197
|
-
resource: nil,
|
198
|
-
errors: errors_from_resource(resource)
|
199
|
-
}
|
200
|
-
end
|
201
|
-
else
|
202
|
-
{
|
203
|
-
resource: nil,
|
204
|
-
errors: { resource: "Unable to find #{scoped_graphql_name}" }
|
205
|
-
}
|
206
|
-
end
|
207
|
-
end
|
208
|
-
end
|
209
|
-
|
210
|
-
mutation_klasses["destroy_#{scoped_graphql_name.underscore}"] = destroy_mutation
|
211
|
-
end
|
94
|
+
mutation_klasses["create_#{graphql_name.underscore}"] = build_create if create
|
95
|
+
mutation_klasses["copy_#{graphql_name.underscore}"] = build_copy if copy
|
96
|
+
mutation_klasses["update_#{graphql_name.underscore}"] = build_update if update
|
97
|
+
mutation_klasses["destroy_#{graphql_name.underscore}"] = build_destroy if destroy
|
212
98
|
end
|
213
99
|
|
214
100
|
def query(**options, &block)
|
@@ -216,10 +102,10 @@ module HQ
|
|
216
102
|
end
|
217
103
|
|
218
104
|
def def_root(field_name, is_array: false, null: true, &block)
|
219
|
-
|
105
|
+
resource = self
|
220
106
|
resolver = -> {
|
221
107
|
Class.new(::GraphQL::Schema::Resolver) do
|
222
|
-
type = is_array ? [
|
108
|
+
type = is_array ? [resource.query_klass] : resource.query_klass
|
223
109
|
type type, null: null
|
224
110
|
class_eval(&block) if block
|
225
111
|
end
|
@@ -229,7 +115,7 @@ module HQ
|
|
229
115
|
}
|
230
116
|
end
|
231
117
|
|
232
|
-
def root_query(find_one: true, find_all: true, pagination:
|
118
|
+
def root_query(find_one: true, find_all: true, pagination: true, limit_max: 250)
|
233
119
|
field_name = graphql_name.underscore
|
234
120
|
scoped_self = self
|
235
121
|
|
@@ -248,18 +134,27 @@ module HQ
|
|
248
134
|
|
249
135
|
if find_all
|
250
136
|
def_root field_name.pluralize, is_array: true, null: false do
|
251
|
-
|
252
|
-
|
137
|
+
if pagination
|
138
|
+
argument :offset, Integer, required: false
|
139
|
+
argument :limit, Integer, required: false
|
140
|
+
end
|
141
|
+
argument :sort_by, scoped_self.sort_fields_enum, required: false
|
142
|
+
argument :sort_order, Enum::SortOrder, required: false
|
253
143
|
|
254
|
-
define_method(:resolve) do |
|
144
|
+
define_method(:resolve) do |limit: nil, offset: nil, sort_by: nil, sort_order: nil, **_attrs|
|
255
145
|
scope = scoped_self.scope(context).all
|
256
146
|
|
257
|
-
if pagination || page ||
|
258
|
-
|
259
|
-
limit = [
|
260
|
-
scope = scope.limit(limit).offset(
|
147
|
+
if pagination || page || limit
|
148
|
+
offset = [0, *offset].max
|
149
|
+
limit = [[limit_max, *limit].min, 0].max
|
150
|
+
scope = scope.limit(limit).offset(offset)
|
261
151
|
end
|
262
152
|
|
153
|
+
sort_by ||= :updated_at
|
154
|
+
sort_order ||= :desc
|
155
|
+
# There should be no risk for SQL injection since an enum is being used for both sort_by and sort_order
|
156
|
+
scope = scope.reorder(sort_by => sort_order)
|
157
|
+
|
263
158
|
scope
|
264
159
|
end
|
265
160
|
end
|
@@ -0,0 +1,159 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module HQ
|
4
|
+
module GraphQL
|
5
|
+
module Resource
|
6
|
+
module AutoMutation
|
7
|
+
def build_create
|
8
|
+
scoped_self = self
|
9
|
+
|
10
|
+
build_mutation(action: :create) do
|
11
|
+
define_method(:resolve) do |**args|
|
12
|
+
resource = scoped_self.new_record(context)
|
13
|
+
resource.assign_attributes(args[:attributes].format_nested_attributes)
|
14
|
+
if resource.save
|
15
|
+
{
|
16
|
+
resource: resource,
|
17
|
+
errors: {},
|
18
|
+
}
|
19
|
+
else
|
20
|
+
{
|
21
|
+
resource: nil,
|
22
|
+
errors: errors_from_resource(resource)
|
23
|
+
}
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
lazy_load do
|
28
|
+
argument :attributes, ::HQ::GraphQL::Inputs[scoped_self.model_name], required: true
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def build_update
|
34
|
+
scoped_self = self
|
35
|
+
|
36
|
+
build_mutation(action: :update, require_primary_key: true) do
|
37
|
+
define_method(:resolve) do |**args|
|
38
|
+
resource = scoped_self.find_record(args, context)
|
39
|
+
|
40
|
+
if resource
|
41
|
+
resource.assign_attributes(args[:attributes].format_nested_attributes)
|
42
|
+
if resource.save
|
43
|
+
{
|
44
|
+
resource: resource,
|
45
|
+
errors: {},
|
46
|
+
}
|
47
|
+
else
|
48
|
+
{
|
49
|
+
resource: nil,
|
50
|
+
errors: errors_from_resource(resource)
|
51
|
+
}
|
52
|
+
end
|
53
|
+
else
|
54
|
+
{
|
55
|
+
resource: nil,
|
56
|
+
errors: { resource: "Unable to find #{self.class.graphql_name}" }
|
57
|
+
}
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
lazy_load do
|
62
|
+
argument :attributes, ::HQ::GraphQL::Inputs[scoped_self.model_name], required: true
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def build_copy
|
68
|
+
scoped_self = self
|
69
|
+
|
70
|
+
build_mutation(action: :copy, require_primary_key: true, nil_klass: true) do
|
71
|
+
define_method(:resolve) do |**args|
|
72
|
+
resource = scoped_self.find_record(args, context)
|
73
|
+
|
74
|
+
if resource
|
75
|
+
copy = resource.copy
|
76
|
+
if copy.save
|
77
|
+
{
|
78
|
+
resource: copy,
|
79
|
+
errors: {},
|
80
|
+
}
|
81
|
+
else
|
82
|
+
{
|
83
|
+
resource: copy,
|
84
|
+
errors: errors_from_resource(copy)
|
85
|
+
}
|
86
|
+
end
|
87
|
+
else
|
88
|
+
{
|
89
|
+
resource: nil,
|
90
|
+
errors: { resource: "Unable to find #{self.class.graphql_name}" }
|
91
|
+
}
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def build_destroy
|
98
|
+
scoped_self = self
|
99
|
+
|
100
|
+
build_mutation(action: :destroy, require_primary_key: true) do
|
101
|
+
define_method(:resolve) do |**attrs|
|
102
|
+
resource = scoped_self.find_record(attrs, context)
|
103
|
+
|
104
|
+
if resource
|
105
|
+
if resource.destroy
|
106
|
+
{
|
107
|
+
resource: resource,
|
108
|
+
errors: {},
|
109
|
+
}
|
110
|
+
else
|
111
|
+
{
|
112
|
+
resource: nil,
|
113
|
+
errors: errors_from_resource(resource)
|
114
|
+
}
|
115
|
+
end
|
116
|
+
else
|
117
|
+
{
|
118
|
+
resource: nil,
|
119
|
+
errors: { resource: "Unable to find #{self.class.graphql_name}" }
|
120
|
+
}
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
def build_mutation(action:, require_primary_key: false, nil_klass: false, &block)
|
127
|
+
gql_name = "#{graphql_name}#{action.to_s.titleize}"
|
128
|
+
scoped_model_name = model_name
|
129
|
+
Class.new(::HQ::GraphQL::Mutation) do
|
130
|
+
graphql_name gql_name
|
131
|
+
|
132
|
+
define_method(:ready?) do |*args|
|
133
|
+
super(*args) && ::HQ::GraphQL.authorized?(action, scoped_model_name, context)
|
134
|
+
end
|
135
|
+
|
136
|
+
lazy_load do
|
137
|
+
field :errors, ::HQ::GraphQL::Types::Object, null: false
|
138
|
+
field :resource, ::HQ::GraphQL::Types[scoped_model_name, nil_klass], null: true
|
139
|
+
end
|
140
|
+
|
141
|
+
instance_eval(&block)
|
142
|
+
|
143
|
+
if require_primary_key
|
144
|
+
lazy_load do
|
145
|
+
klass = scoped_model_name.constantize
|
146
|
+
primary_key = klass.primary_key
|
147
|
+
argument primary_key, ::GraphQL::Types::ID, required: true
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
def errors_from_resource(resource)
|
152
|
+
resource.errors.to_h.deep_transform_keys { |k| k.to_s.camelize(:lower) }
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
data/lib/hq/graphql/types.rb
CHANGED
@@ -64,16 +64,10 @@ module HQ
|
|
64
64
|
|
65
65
|
def find_klass(klass_or_string, method)
|
66
66
|
klass = klass_or_string.is_a?(String) ? klass_or_string.constantize : klass_or_string
|
67
|
-
|
68
|
-
type ||= find_type(klass.base_class)
|
69
|
-
type ||= find_type(klass.superclass)
|
67
|
+
resource = ::HQ::GraphQL.lookup_resource(klass)
|
70
68
|
|
71
|
-
raise(Error, Error::MISSING_TYPE_MSG % { klass: klass.name }) if !
|
72
|
-
|
73
|
-
end
|
74
|
-
|
75
|
-
def find_type(klass)
|
76
|
-
::HQ::GraphQL.resource_lookup(klass) || ::HQ::GraphQL.types.detect { |t| t.model_klass == klass }
|
69
|
+
raise(Error, Error::MISSING_TYPE_MSG % { klass: klass.name }) if !resource
|
70
|
+
resource.send(method)
|
77
71
|
end
|
78
72
|
end
|
79
73
|
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.0
|
4
|
+
version: 2.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Danny Jones
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2020-
|
11
|
+
date: 2020-06-17 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|
@@ -252,13 +252,16 @@ files:
|
|
252
252
|
- lib/hq/graphql/config.rb
|
253
253
|
- lib/hq/graphql/engine.rb
|
254
254
|
- lib/hq/graphql/enum.rb
|
255
|
+
- lib/hq/graphql/enum/sort_by.rb
|
256
|
+
- lib/hq/graphql/enum/sort_order.rb
|
255
257
|
- lib/hq/graphql/field.rb
|
256
258
|
- lib/hq/graphql/input_object.rb
|
257
259
|
- lib/hq/graphql/inputs.rb
|
258
260
|
- lib/hq/graphql/mutation.rb
|
259
261
|
- lib/hq/graphql/object.rb
|
262
|
+
- lib/hq/graphql/paginated_association_loader.rb
|
260
263
|
- lib/hq/graphql/resource.rb
|
261
|
-
- lib/hq/graphql/resource/
|
264
|
+
- lib/hq/graphql/resource/auto_mutation.rb
|
262
265
|
- lib/hq/graphql/root_mutation.rb
|
263
266
|
- lib/hq/graphql/root_query.rb
|
264
267
|
- lib/hq/graphql/scalars.rb
|
@@ -1,38 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module HQ
|
4
|
-
module GraphQL
|
5
|
-
module Resource
|
6
|
-
module Mutation
|
7
|
-
def self.build(model_name, action:, graphql_name:, require_primary_key: false, nil_klass: false, &block)
|
8
|
-
Class.new(::HQ::GraphQL::Mutation) do
|
9
|
-
graphql_name graphql_name
|
10
|
-
|
11
|
-
define_method(:ready?) do |*args|
|
12
|
-
super(*args) && ::HQ::GraphQL.authorized?(action, model_name, context)
|
13
|
-
end
|
14
|
-
|
15
|
-
lazy_load do
|
16
|
-
field :errors, ::HQ::GraphQL::Types::Object, null: false
|
17
|
-
field :resource, ::HQ::GraphQL::Types[model_name, nil_klass], null: true
|
18
|
-
end
|
19
|
-
|
20
|
-
instance_eval(&block)
|
21
|
-
|
22
|
-
if require_primary_key
|
23
|
-
lazy_load do
|
24
|
-
klass = model_name.constantize
|
25
|
-
primary_key = klass.primary_key
|
26
|
-
argument primary_key, ::GraphQL::Types::ID, required: true
|
27
|
-
end
|
28
|
-
end
|
29
|
-
|
30
|
-
def errors_from_resource(resource)
|
31
|
-
resource.errors.to_h.deep_transform_keys { |k| k.to_s.camelize(:lower) }
|
32
|
-
end
|
33
|
-
end
|
34
|
-
end
|
35
|
-
end
|
36
|
-
end
|
37
|
-
end
|
38
|
-
end
|