hq-graphql 2.0.9 → 2.1.2
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 +17 -7
- data/lib/hq/graphql/association_loader.rb +49 -0
- data/lib/hq/graphql/config.rb +11 -2
- data/lib/hq/graphql/enum.rb +2 -0
- data/lib/hq/graphql/enum/sort_by.rb +10 -0
- data/lib/hq/graphql/enum/sort_order.rb +10 -0
- data/lib/hq/graphql/field.rb +12 -10
- data/lib/hq/graphql/field_extension/association_loader_extension.rb +15 -0
- data/lib/hq/graphql/field_extension/paginated_arguments.rb +22 -0
- data/lib/hq/graphql/field_extension/paginated_loader.rb +45 -0
- data/lib/hq/graphql/input_object.rb +4 -0
- data/lib/hq/graphql/inputs.rb +3 -7
- data/lib/hq/graphql/object.rb +40 -9
- data/lib/hq/graphql/object_association.rb +50 -0
- data/lib/hq/graphql/paginated_association_loader.rb +158 -0
- data/lib/hq/graphql/resource.rb +46 -151
- data/lib/hq/graphql/resource/auto_mutation.rb +163 -0
- data/lib/hq/graphql/root_mutation.rb +1 -1
- data/lib/hq/graphql/types.rb +10 -9
- data/lib/hq/graphql/version.rb +1 -1
- metadata +11 -5
- data/lib/hq/graphql/loaders.rb +0 -3
- data/lib/hq/graphql/loaders/association.rb +0 -51
- 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: 8851537c95e46beea01cd929ec57a5c29a0649eeb059e7f35b5bef1fe71f6da3
|
4
|
+
data.tar.gz: 31008afafa861554a0327285681191a4107c83faf7d3c790f402fe88056d2ca6
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f63e936bed969d901e14265ac4fe284d4ef18838cf27c6f4c5a2575d6b0c07e3e4d533e0ea2e8a402777351b98b3d2c0fd07b021781bfb9e618518a0b60d7988
|
7
|
+
data.tar.gz: 76b08ade067128ab0a4d96449751481413c8a6e714ae9d69c73817a5692468dee0f279d3c13456f504f145ee6f713418714b258b23540193eed48b4d0278e3e8
|
data/lib/hq/graphql.rb
CHANGED
@@ -28,14 +28,24 @@ module HQ
|
|
28
28
|
config.default_scope.call(scope, context)
|
29
29
|
end
|
30
30
|
|
31
|
-
def self.
|
32
|
-
config.
|
31
|
+
def self.extract_class(klass)
|
32
|
+
config.extract_class.call(klass)
|
33
|
+
end
|
34
|
+
|
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
|
33
43
|
end
|
34
44
|
|
35
45
|
def self.reset!
|
36
46
|
@root_queries = nil
|
37
47
|
@enums = nil
|
38
|
-
@
|
48
|
+
@resources = nil
|
39
49
|
::HQ::GraphQL::Inputs.reset!
|
40
50
|
::HQ::GraphQL::Types.reset!
|
41
51
|
end
|
@@ -48,21 +58,21 @@ module HQ
|
|
48
58
|
@enums ||= Set.new
|
49
59
|
end
|
50
60
|
|
51
|
-
def self.
|
52
|
-
@
|
61
|
+
def self.resources
|
62
|
+
@resources ||= Set.new
|
53
63
|
end
|
54
64
|
end
|
55
65
|
end
|
56
66
|
|
57
|
-
require "hq/graphql/
|
67
|
+
require "hq/graphql/association_loader"
|
58
68
|
require "hq/graphql/scalars"
|
59
69
|
require "hq/graphql/comparator"
|
60
70
|
require "hq/graphql/enum"
|
61
71
|
require "hq/graphql/inputs"
|
62
72
|
require "hq/graphql/input_object"
|
63
|
-
require "hq/graphql/loaders"
|
64
73
|
require "hq/graphql/mutation"
|
65
74
|
require "hq/graphql/object"
|
75
|
+
require "hq/graphql/paginated_association_loader"
|
66
76
|
require "hq/graphql/resource"
|
67
77
|
require "hq/graphql/root_mutation"
|
68
78
|
require "hq/graphql/root_query"
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module HQ
|
4
|
+
module GraphQL
|
5
|
+
class AssociationLoader < ::GraphQL::Batch::Loader
|
6
|
+
def initialize(model, association_name)
|
7
|
+
@model = model
|
8
|
+
@association_name = association_name
|
9
|
+
validate
|
10
|
+
end
|
11
|
+
|
12
|
+
def load(record)
|
13
|
+
raise TypeError, "#{@model} loader can't load association for #{record.class}" unless record.is_a?(@model)
|
14
|
+
return Promise.resolve(read_association(record)) if association_loaded?(record)
|
15
|
+
super
|
16
|
+
end
|
17
|
+
|
18
|
+
# We want to load the associations on all records, even if they have the same id
|
19
|
+
def cache_key(record)
|
20
|
+
record.object_id
|
21
|
+
end
|
22
|
+
|
23
|
+
def perform(records)
|
24
|
+
preload_association(records)
|
25
|
+
records.each { |record| fulfill(record, read_association(record)) }
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def validate
|
31
|
+
unless @model.reflect_on_association(@association_name)
|
32
|
+
raise ArgumentError, "No association #{@association_name} on #{@model}"
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def preload_association(records)
|
37
|
+
::ActiveRecord::Associations::Preloader.new.preload(records, @association_name)
|
38
|
+
end
|
39
|
+
|
40
|
+
def read_association(record)
|
41
|
+
record.public_send(@association_name)
|
42
|
+
end
|
43
|
+
|
44
|
+
def association_loaded?(record)
|
45
|
+
record.association(@association_name).loaded?
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
data/lib/hq/graphql/config.rb
CHANGED
@@ -2,10 +2,19 @@
|
|
2
2
|
|
3
3
|
module HQ
|
4
4
|
module GraphQL
|
5
|
-
class Config < Struct.new(
|
5
|
+
class Config < Struct.new(
|
6
|
+
:authorize,
|
7
|
+
:authorize_field,
|
8
|
+
:default_scope,
|
9
|
+
:extract_class,
|
10
|
+
:resource_lookup,
|
11
|
+
:use_experimental_associations,
|
12
|
+
keyword_init: true
|
13
|
+
)
|
6
14
|
def initialize(
|
7
15
|
default_scope: ->(scope, _context) { scope },
|
8
|
-
|
16
|
+
extract_class: ->(klass) { klass.to_s.gsub(/^Resources|Resource$/, "") },
|
17
|
+
resource_lookup: ->(klass) { "::Resources::#{klass}Resource".safe_constantize || "::Resources::#{klass}".safe_constantize },
|
9
18
|
**options
|
10
19
|
)
|
11
20
|
super
|
data/lib/hq/graphql/enum.rb
CHANGED
data/lib/hq/graphql/field.rb
CHANGED
@@ -3,13 +3,21 @@
|
|
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
|
+
@klass_or_string = klass
|
13
|
+
end
|
14
|
+
|
15
|
+
def scope(&block)
|
16
|
+
if block
|
17
|
+
@scope = block
|
18
|
+
else
|
19
|
+
@scope
|
20
|
+
end
|
13
21
|
end
|
14
22
|
|
15
23
|
def authorized?(object, ctx)
|
@@ -18,14 +26,8 @@ module HQ
|
|
18
26
|
::HQ::GraphQL.authorize_field(authorize_action, self, object, ctx)
|
19
27
|
end
|
20
28
|
|
21
|
-
def
|
22
|
-
|
23
|
-
Loaders::Association.for(klass.constantize, original_name).load(object.object).then do
|
24
|
-
super
|
25
|
-
end
|
26
|
-
else
|
27
|
-
super
|
28
|
-
end
|
29
|
+
def klass
|
30
|
+
@klass ||= @klass_or_string.is_a?(String) ? @klass_or_string.constantize : @klass_or_string
|
29
31
|
end
|
30
32
|
end
|
31
33
|
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "hq/graphql/association_loader"
|
4
|
+
|
5
|
+
module HQ
|
6
|
+
module GraphQL
|
7
|
+
module FieldExtension
|
8
|
+
class AssociationLoaderExtension < ::GraphQL::Schema::FieldExtension
|
9
|
+
def resolve(object:, **_kwargs)
|
10
|
+
AssociationLoader.for(options[:klass], field.original_name).load(object.object)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "hq/graphql/enum/sort_by"
|
4
|
+
require "hq/graphql/enum/sort_order"
|
5
|
+
|
6
|
+
module HQ
|
7
|
+
module GraphQL
|
8
|
+
module FieldExtension
|
9
|
+
class PaginatedArguments < ::GraphQL::Schema::FieldExtension
|
10
|
+
def apply
|
11
|
+
field.argument :offset, Integer, required: false
|
12
|
+
field.argument :limit, Integer, required: false
|
13
|
+
field.argument :sort_order, Enum::SortOrder, required: false
|
14
|
+
|
15
|
+
resource = ::HQ::GraphQL.lookup_resource(options[:klass])
|
16
|
+
enum = resource ? resource.sort_fields_enum : ::HQ::GraphQL::Enum::SortBy
|
17
|
+
field.argument :sort_by, enum, required: false
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "hq/graphql/paginated_association_loader"
|
4
|
+
|
5
|
+
module HQ
|
6
|
+
module GraphQL
|
7
|
+
module FieldExtension
|
8
|
+
class PaginatedLoader < ::GraphQL::Schema::FieldExtension
|
9
|
+
def resolve(object:, arguments:, **_options)
|
10
|
+
limit = arguments[:limit]
|
11
|
+
offset = arguments[:offset]
|
12
|
+
sort_by = arguments[:sort_by]
|
13
|
+
sort_order = arguments[:sort_order]
|
14
|
+
scope = field.scope.call(**arguments.except(:limit, :offset, :sort_by, :sort_order)) if field.scope
|
15
|
+
loader = PaginatedAssociationLoader.for(
|
16
|
+
klass,
|
17
|
+
association,
|
18
|
+
internal_association: internal_association,
|
19
|
+
scope: scope,
|
20
|
+
limit: limit,
|
21
|
+
offset: offset,
|
22
|
+
sort_by: sort_by,
|
23
|
+
sort_order: sort_order
|
24
|
+
)
|
25
|
+
|
26
|
+
loader.load(object.object)
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def association
|
32
|
+
options[:association]
|
33
|
+
end
|
34
|
+
|
35
|
+
def internal_association
|
36
|
+
options[:internal_association]
|
37
|
+
end
|
38
|
+
|
39
|
+
def klass
|
40
|
+
options[:klass]
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
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
@@ -1,12 +1,21 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "hq/graphql/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
|
+
|
3
11
|
module HQ
|
4
12
|
module GraphQL
|
5
13
|
class Object < ::GraphQL::Schema::Object
|
6
14
|
include Scalars
|
7
|
-
include
|
15
|
+
include ActiveRecordExtensions
|
16
|
+
extend ObjectAssociation
|
8
17
|
|
9
|
-
field_class
|
18
|
+
field_class Field
|
10
19
|
|
11
20
|
def self.authorize_action(action)
|
12
21
|
self.authorized_action = action
|
@@ -28,8 +37,15 @@ module HQ
|
|
28
37
|
end
|
29
38
|
|
30
39
|
model_associations.each do |association|
|
40
|
+
next if resource_reflections[association.name.to_s]
|
31
41
|
field_from_association(association, auto_nil: auto_nil)
|
32
42
|
end
|
43
|
+
|
44
|
+
resource_reflections.values.each do |resource_reflection|
|
45
|
+
reflection = resource_reflection.reflection(model_klass)
|
46
|
+
next unless reflection
|
47
|
+
field_from_association(reflection, auto_nil: auto_nil, internal_association: true, &resource_reflection.block)
|
48
|
+
end
|
33
49
|
end
|
34
50
|
end
|
35
51
|
|
@@ -46,21 +62,36 @@ module HQ
|
|
46
62
|
@authorized_action ||= :read
|
47
63
|
end
|
48
64
|
|
49
|
-
def field_from_association(association, auto_nil:)
|
50
|
-
|
51
|
-
|
65
|
+
def field_from_association(association, auto_nil:, internal_association: false, &block)
|
66
|
+
# The PaginationAssociationLoader doesn't support through associations yet
|
67
|
+
return if association.through_reflection? && ::HQ::GraphQL.use_experimental_associations?
|
68
|
+
|
69
|
+
association_klass = association.klass
|
70
|
+
name = association.name
|
71
|
+
klass = model_klass
|
72
|
+
type = Types[association_klass]
|
52
73
|
case association.macro
|
53
74
|
when :has_many
|
54
|
-
field name, [type], null: false, klass: model_name
|
75
|
+
field name, [type], null: false, klass: model_name do
|
76
|
+
if ::HQ::GraphQL.use_experimental_associations?
|
77
|
+
extension FieldExtension::PaginatedArguments, klass: association_klass
|
78
|
+
extension FieldExtension::PaginatedLoader, klass: klass, association: name, internal_association: internal_association
|
79
|
+
else
|
80
|
+
extension FieldExtension::AssociationLoaderExtension, klass: klass
|
81
|
+
end
|
82
|
+
instance_eval(&block) if block
|
83
|
+
end
|
55
84
|
else
|
56
|
-
field name, type, null: !auto_nil || !association_required?(association), klass: model_name
|
85
|
+
field name, type, null: !auto_nil || !association_required?(association), klass: model_name do
|
86
|
+
extension FieldExtension::AssociationLoaderExtension, klass: klass
|
87
|
+
end
|
57
88
|
end
|
58
|
-
rescue
|
89
|
+
rescue Types::Error
|
59
90
|
nil
|
60
91
|
end
|
61
92
|
|
62
93
|
def field_from_column(column, auto_nil:)
|
63
|
-
field column.name,
|
94
|
+
field column.name, Types.type_from_column(column), null: !auto_nil || column.null
|
64
95
|
end
|
65
96
|
|
66
97
|
def association_required?(association)
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module HQ
|
4
|
+
module GraphQL
|
5
|
+
module ObjectAssociation
|
6
|
+
class ResourceReflection
|
7
|
+
attr_reader :name, :scope, :options, :macro, :block
|
8
|
+
|
9
|
+
def initialize(name, scope, options, macro, block)
|
10
|
+
@name = name
|
11
|
+
@scope = scope
|
12
|
+
@options = options
|
13
|
+
@macro = macro
|
14
|
+
@block = block
|
15
|
+
end
|
16
|
+
|
17
|
+
def reflection(model_klass)
|
18
|
+
if macro == :has_many
|
19
|
+
::ActiveRecord::Associations::Builder::HasMany.create_reflection(model_klass, name, scope, options)
|
20
|
+
elsif macro == :belongs_to
|
21
|
+
::ActiveRecord::Associations::Builder::BelongsTo.create_reflection(model_klass, name, scope, options)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def reflect_on_association(association)
|
27
|
+
resource_reflections[association.to_s]&.reflection(model_klass)
|
28
|
+
end
|
29
|
+
|
30
|
+
def belongs_to(name, scope = nil, **options, &block)
|
31
|
+
add_reflection(name, scope, options, :belongs_to, block)
|
32
|
+
end
|
33
|
+
|
34
|
+
def has_many(name, scope = nil, through: nil, **options, &block)
|
35
|
+
raise TypeError, "has_many through is unsupported" if through
|
36
|
+
add_reflection(name, scope, options, :has_many, block)
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def resource_reflections
|
42
|
+
@resource_reflections ||= {}
|
43
|
+
end
|
44
|
+
|
45
|
+
def add_reflection(name, scope, options, macro, block)
|
46
|
+
resource_reflections[name.to_s] = ResourceReflection.new(name, scope, options, macro, block)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,158 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "hq/graphql/types"
|
4
|
+
|
5
|
+
module HQ
|
6
|
+
module GraphQL
|
7
|
+
class PaginatedAssociationLoader < ::GraphQL::Batch::Loader
|
8
|
+
def self.for(*args, scope: nil, **kwargs)
|
9
|
+
if scope
|
10
|
+
raise TypeError, "scope must be an ActiveRecord::Relation" unless scope.is_a?(::ActiveRecord::Relation)
|
11
|
+
executor = ::GraphQL::Batch::Executor.current
|
12
|
+
loader_key = loader_key_for(*args, **kwargs, scope: scope.to_sql)
|
13
|
+
executor.loader(loader_key) { new(*args, **kwargs, scope: scope) }
|
14
|
+
else
|
15
|
+
super
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def initialize(model, association_name, internal_association: false, limit: nil, offset: nil, scope: nil, sort_by: nil, sort_order: nil)
|
20
|
+
@model = model
|
21
|
+
@association_name = association_name
|
22
|
+
@internal_association = internal_association
|
23
|
+
@limit = [0, limit].max if limit
|
24
|
+
@offset = [0, offset].max if offset
|
25
|
+
@scope = scope
|
26
|
+
@sort_by = sort_by || :updated_at
|
27
|
+
@sort_order = normalize_sort_order(sort_order)
|
28
|
+
|
29
|
+
validate!
|
30
|
+
end
|
31
|
+
|
32
|
+
def load(record)
|
33
|
+
raise TypeError, "#{@model} loader can't load association for #{record.class}" unless record.is_a?(@model)
|
34
|
+
super
|
35
|
+
end
|
36
|
+
|
37
|
+
def cache_key(record)
|
38
|
+
record.send(primary_key)
|
39
|
+
end
|
40
|
+
|
41
|
+
def perform(records)
|
42
|
+
scope =
|
43
|
+
if @limit || @offset
|
44
|
+
# If a limit or offset is added, then we need to transform the query
|
45
|
+
# into a lateral join so that we can limit on groups of data.
|
46
|
+
#
|
47
|
+
# > SELECT * FROM addresses WHERE addresses.user_id IN ($1, $2, ..., $N) ORDER BY addresses.created_at DESC;
|
48
|
+
# ...becomes
|
49
|
+
# > SELECT DISTINCT a_top.*
|
50
|
+
# > FROM addresses
|
51
|
+
# > INNER JOIN LATERAL (
|
52
|
+
# > SELECT inner.*
|
53
|
+
# > FROM addresses inner
|
54
|
+
# > WHERE inner.user_id = addresses.user_id
|
55
|
+
# > ORDER BY inner.created_at DESC
|
56
|
+
# > LIMIT 1
|
57
|
+
# > ) a_top ON TRUE
|
58
|
+
# > WHERE addresses.user_id IN ($1, $2, ..., $N)
|
59
|
+
# > ORDER BY a_top.created_at DESC
|
60
|
+
inner_table = association_class.arel_table
|
61
|
+
association_table = inner_table.alias("outer")
|
62
|
+
|
63
|
+
inside_scope = default_scope.
|
64
|
+
select(inner_table[::Arel.star]).
|
65
|
+
from(inner_table).
|
66
|
+
where(inner_table[association_key].eq(association_table[association_key])).
|
67
|
+
reorder(arel_order(inner_table)).
|
68
|
+
limit(@limit).
|
69
|
+
offset(@offset)
|
70
|
+
|
71
|
+
outside_table = ::Arel::Table.new("top")
|
72
|
+
association_class.
|
73
|
+
select(outside_table[::Arel.star]).distinct.
|
74
|
+
from(association_table).
|
75
|
+
joins("INNER JOIN LATERAL (#{inside_scope.to_sql}) #{outside_table.name} ON TRUE").
|
76
|
+
where(association_table[association_key].in(records.map { |r| join_value(r) })).
|
77
|
+
reorder(arel_order(outside_table))
|
78
|
+
else
|
79
|
+
default_scope.
|
80
|
+
reorder(arel_order(association_class.arel_table)).
|
81
|
+
where(association_key => records.map { |r| join_value(r) })
|
82
|
+
end
|
83
|
+
|
84
|
+
results = scope.to_a
|
85
|
+
records.each do |record|
|
86
|
+
fulfill(record, association_value(record, results)) unless fulfilled?(record)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
private
|
91
|
+
|
92
|
+
def association_key
|
93
|
+
belongs_to? ? association.association_primary_key : association.foreign_key
|
94
|
+
end
|
95
|
+
|
96
|
+
def association_value(record, results)
|
97
|
+
enumerator = has_many? ? :select : :detect
|
98
|
+
results.send(enumerator) { |r| r.send(association_key) == join_value(record) }
|
99
|
+
end
|
100
|
+
|
101
|
+
def join_key
|
102
|
+
belongs_to? ? association.foreign_key : association.association_primary_key
|
103
|
+
end
|
104
|
+
|
105
|
+
def join_value(record)
|
106
|
+
record.send(join_key)
|
107
|
+
end
|
108
|
+
|
109
|
+
def default_scope
|
110
|
+
scope = association_class
|
111
|
+
scope = association.scopes.reduce(scope, &:merge)
|
112
|
+
scope = association_class.default_scopes.reduce(scope, &:merge)
|
113
|
+
scope = scope.merge(@scope) if @scope
|
114
|
+
scope
|
115
|
+
end
|
116
|
+
|
117
|
+
def belongs_to?
|
118
|
+
association.macro == :belongs_to
|
119
|
+
end
|
120
|
+
|
121
|
+
def has_many?
|
122
|
+
association.macro == :has_many
|
123
|
+
end
|
124
|
+
|
125
|
+
def association
|
126
|
+
if @internal_association
|
127
|
+
Types[@model].reflect_on_association(@association_name)
|
128
|
+
else
|
129
|
+
@model.reflect_on_association(@association_name)
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
def association_class
|
134
|
+
association.klass
|
135
|
+
end
|
136
|
+
|
137
|
+
def primary_key
|
138
|
+
@model.primary_key
|
139
|
+
end
|
140
|
+
|
141
|
+
def arel_order(table)
|
142
|
+
table[@sort_by].send(@sort_order)
|
143
|
+
end
|
144
|
+
|
145
|
+
def normalize_sort_order(input)
|
146
|
+
if input.to_s.casecmp("asc").zero?
|
147
|
+
:asc
|
148
|
+
else
|
149
|
+
:desc
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
def validate!
|
154
|
+
raise ArgumentError, "No association #{@association_name} on #{@model}" unless association
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
data/lib/hq/graphql/resource.rb
CHANGED
@@ -1,19 +1,26 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "hq/graphql/
|
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"
|
4
9
|
|
5
10
|
module HQ
|
6
11
|
module GraphQL
|
7
12
|
module Resource
|
8
13
|
def self.included(base)
|
9
14
|
super
|
10
|
-
::HQ::GraphQL.
|
15
|
+
::HQ::GraphQL.resources << base
|
11
16
|
base.include Scalars
|
12
17
|
base.include ::GraphQL::Types
|
13
18
|
base.extend ClassMethods
|
14
19
|
end
|
15
20
|
|
16
21
|
module ClassMethods
|
22
|
+
include AutoMutation
|
23
|
+
|
17
24
|
attr_writer :graphql_name, :model_name
|
18
25
|
|
19
26
|
def scope(context)
|
@@ -37,7 +44,7 @@ module HQ
|
|
37
44
|
end
|
38
45
|
|
39
46
|
def model_name
|
40
|
-
@model_name ||
|
47
|
+
@model_name || ::HQ::GraphQL.extract_class(self)
|
41
48
|
end
|
42
49
|
|
43
50
|
def model_klass
|
@@ -60,6 +67,10 @@ module HQ
|
|
60
67
|
@query_klass ||= build_graphql_object
|
61
68
|
end
|
62
69
|
|
70
|
+
def sort_fields_enum
|
71
|
+
@sort_fields_enum || ::HQ::GraphQL::Enum::SortBy
|
72
|
+
end
|
73
|
+
|
63
74
|
protected
|
64
75
|
|
65
76
|
def default_scope(&block)
|
@@ -71,155 +82,25 @@ module HQ
|
|
71
82
|
end
|
72
83
|
|
73
84
|
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
|
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
|
212
89
|
end
|
213
90
|
|
214
91
|
def query(**options, &block)
|
215
92
|
@query_klass = build_graphql_object(**options, &block)
|
216
93
|
end
|
217
94
|
|
95
|
+
def sort_fields(*fields)
|
96
|
+
self.sort_fields_enum = fields
|
97
|
+
end
|
98
|
+
|
218
99
|
def def_root(field_name, is_array: false, null: true, &block)
|
219
|
-
|
100
|
+
resource = self
|
220
101
|
resolver = -> {
|
221
102
|
Class.new(::GraphQL::Schema::Resolver) do
|
222
|
-
type = is_array ? [
|
103
|
+
type = is_array ? [resource.query_klass] : resource.query_klass
|
223
104
|
type type, null: null
|
224
105
|
class_eval(&block) if block
|
225
106
|
end
|
@@ -229,7 +110,7 @@ module HQ
|
|
229
110
|
}
|
230
111
|
end
|
231
112
|
|
232
|
-
def root_query(find_one: true, find_all: true, pagination:
|
113
|
+
def root_query(find_one: true, find_all: true, pagination: true, limit_max: 250)
|
233
114
|
field_name = graphql_name.underscore
|
234
115
|
scoped_self = self
|
235
116
|
|
@@ -248,18 +129,22 @@ module HQ
|
|
248
129
|
|
249
130
|
if find_all
|
250
131
|
def_root field_name.pluralize, is_array: true, null: false do
|
251
|
-
|
252
|
-
argument :per_page, Integer, required: false
|
132
|
+
extension FieldExtension::PaginatedArguments, klass: scoped_self.model_klass if pagination
|
253
133
|
|
254
|
-
define_method(:resolve) do |
|
134
|
+
define_method(:resolve) do |limit: nil, offset: nil, sort_by: nil, sort_order: nil, **_attrs|
|
255
135
|
scope = scoped_self.scope(context).all
|
256
136
|
|
257
|
-
if pagination || page ||
|
258
|
-
|
259
|
-
limit = [
|
260
|
-
scope = scope.limit(limit).offset(
|
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)
|
261
141
|
end
|
262
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
|
+
|
263
148
|
scope
|
264
149
|
end
|
265
150
|
end
|
@@ -291,6 +176,16 @@ module HQ
|
|
291
176
|
class_eval(&block) if block
|
292
177
|
end
|
293
178
|
end
|
179
|
+
|
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
|
294
189
|
end
|
295
190
|
end
|
296
191
|
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
|
data/lib/hq/graphql/types.rb
CHANGED
@@ -1,5 +1,8 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "hq/graphql/types/object"
|
4
|
+
require "hq/graphql/types/uuid"
|
5
|
+
|
3
6
|
module HQ
|
4
7
|
module GraphQL
|
5
8
|
module Types
|
@@ -39,6 +42,10 @@ module HQ
|
|
39
42
|
::GraphQL::Types::Float
|
40
43
|
when :boolean
|
41
44
|
::GraphQL::Types::Boolean
|
45
|
+
when :date
|
46
|
+
::GraphQL::Types::ISO8601Date
|
47
|
+
when :datetime
|
48
|
+
::GraphQL::Types::ISO8601DateTime
|
42
49
|
else
|
43
50
|
::GraphQL::Types::String
|
44
51
|
end
|
@@ -64,16 +71,10 @@ module HQ
|
|
64
71
|
|
65
72
|
def find_klass(klass_or_string, method)
|
66
73
|
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)
|
70
|
-
|
71
|
-
raise(Error, Error::MISSING_TYPE_MSG % { klass: klass.name }) if !type
|
72
|
-
type.send(method)
|
73
|
-
end
|
74
|
+
resource = ::HQ::GraphQL.lookup_resource(klass)
|
74
75
|
|
75
|
-
|
76
|
-
|
76
|
+
raise(Error, Error::MISSING_TYPE_MSG % { klass: klass.name }) if !resource
|
77
|
+
resource.send(method)
|
77
78
|
end
|
78
79
|
end
|
79
80
|
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.
|
4
|
+
version: 2.1.2
|
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-23 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|
@@ -247,19 +247,25 @@ files:
|
|
247
247
|
- lib/hq-graphql.rb
|
248
248
|
- lib/hq/graphql.rb
|
249
249
|
- lib/hq/graphql/active_record_extensions.rb
|
250
|
+
- lib/hq/graphql/association_loader.rb
|
250
251
|
- lib/hq/graphql/comparator.rb
|
251
252
|
- lib/hq/graphql/config.rb
|
252
253
|
- lib/hq/graphql/engine.rb
|
253
254
|
- lib/hq/graphql/enum.rb
|
255
|
+
- lib/hq/graphql/enum/sort_by.rb
|
256
|
+
- lib/hq/graphql/enum/sort_order.rb
|
254
257
|
- lib/hq/graphql/field.rb
|
258
|
+
- lib/hq/graphql/field_extension/association_loader_extension.rb
|
259
|
+
- lib/hq/graphql/field_extension/paginated_arguments.rb
|
260
|
+
- lib/hq/graphql/field_extension/paginated_loader.rb
|
255
261
|
- lib/hq/graphql/input_object.rb
|
256
262
|
- lib/hq/graphql/inputs.rb
|
257
|
-
- lib/hq/graphql/loaders.rb
|
258
|
-
- lib/hq/graphql/loaders/association.rb
|
259
263
|
- lib/hq/graphql/mutation.rb
|
260
264
|
- lib/hq/graphql/object.rb
|
265
|
+
- lib/hq/graphql/object_association.rb
|
266
|
+
- lib/hq/graphql/paginated_association_loader.rb
|
261
267
|
- lib/hq/graphql/resource.rb
|
262
|
-
- lib/hq/graphql/resource/
|
268
|
+
- lib/hq/graphql/resource/auto_mutation.rb
|
263
269
|
- lib/hq/graphql/root_mutation.rb
|
264
270
|
- lib/hq/graphql/root_query.rb
|
265
271
|
- lib/hq/graphql/scalars.rb
|
data/lib/hq/graphql/loaders.rb
DELETED
@@ -1,51 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module HQ
|
4
|
-
module GraphQL
|
5
|
-
module Loaders
|
6
|
-
class Association < ::GraphQL::Batch::Loader
|
7
|
-
def initialize(model, association_name)
|
8
|
-
@model = model
|
9
|
-
@association_name = association_name
|
10
|
-
validate
|
11
|
-
end
|
12
|
-
|
13
|
-
def load(record)
|
14
|
-
raise TypeError, "#{@model} loader can't load association for #{record.class}" unless record.is_a?(@model)
|
15
|
-
return Promise.resolve(read_association(record)) if association_loaded?(record)
|
16
|
-
super
|
17
|
-
end
|
18
|
-
|
19
|
-
# We want to load the associations on all records, even if they have the same id
|
20
|
-
def cache_key(record)
|
21
|
-
record.object_id
|
22
|
-
end
|
23
|
-
|
24
|
-
def perform(records)
|
25
|
-
preload_association(records)
|
26
|
-
records.each { |record| fulfill(record, read_association(record)) }
|
27
|
-
end
|
28
|
-
|
29
|
-
private
|
30
|
-
|
31
|
-
def validate
|
32
|
-
unless @model.reflect_on_association(@association_name)
|
33
|
-
raise ArgumentError, "No association #{@association_name} on #{@model}"
|
34
|
-
end
|
35
|
-
end
|
36
|
-
|
37
|
-
def preload_association(records)
|
38
|
-
::ActiveRecord::Associations::Preloader.new.preload(records, @association_name)
|
39
|
-
end
|
40
|
-
|
41
|
-
def read_association(record)
|
42
|
-
record.public_send(@association_name)
|
43
|
-
end
|
44
|
-
|
45
|
-
def association_loaded?(record)
|
46
|
-
record.association(@association_name).loaded?
|
47
|
-
end
|
48
|
-
end
|
49
|
-
end
|
50
|
-
end
|
51
|
-
end
|
@@ -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
|