hq-graphql 2.0.10 → 2.1.3

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fbfd2d5dad56008b05618a89354c42ced0ddac3ee44d292b96773303488171d8
4
- data.tar.gz: 862fee899d0964af9177327b73ad7befcc200985f4f6323272ebb9d9122f5a5a
3
+ metadata.gz: 50ee2af7af4309a1fb549c15b88abdb8bda3454eacfdce5136f1997b814c52fb
4
+ data.tar.gz: 9dc0b893d4ad8bf8a999cba996224488be9c394e55f83ef979460ac19f96c381
5
5
  SHA512:
6
- metadata.gz: d58e0f041362550a8aea5d3a0f03ca1ba0be02ad034b8c3def9d807f3c7045913c900ee79f3058bdf2d9396388d22a236b3ff6e368fb9d4758fcde60abb92ae5
7
- data.tar.gz: 305b4e50bbb1f025aa2a977e45eba2aa63091b322396ac0bcfb2ce3be3f9ba28dd166097fe594982bc705dc701220d4b5b924721eee1ed65bc18387a3c2529ac
6
+ metadata.gz: f4940761bdccfd01c867e21f69ab66a5459d8b151024ebd79679906ea62fa6c5d2208ce311a339ed720021c2b43221e51ebb092de8f4bff87a15bc3e3f07f376
7
+ data.tar.gz: 00a7683630411f065d954132918b3449ef6aec9419de30702dd43bf99ad34fb8dbaf5ee1ae248759f3029006db3863257fe86a99d7b89a7456123c82d52ef7b3
@@ -8,6 +8,10 @@ require "hq/graphql/config"
8
8
 
9
9
  module HQ
10
10
  module GraphQL
11
+ class << self
12
+ delegate :default_object_class, to: :config
13
+ end
14
+
11
15
  def self.config
12
16
  @config ||= ::HQ::GraphQL::Config.new
13
17
  end
@@ -32,14 +36,20 @@ module HQ
32
36
  config.extract_class.call(klass)
33
37
  end
34
38
 
35
- def self.resource_lookup(klass)
36
- config.resource_lookup.call(klass)
39
+ def self.lookup_resource(klass)
40
+ [klass, klass.base_class, klass.superclass].lazy.map do |k|
41
+ config.resource_lookup.call(k) || resources.detect { |r| r.model_klass == k }
42
+ end.reject(&:nil?).first
43
+ end
44
+
45
+ def self.use_experimental_associations?
46
+ !!config.use_experimental_associations
37
47
  end
38
48
 
39
49
  def self.reset!
40
50
  @root_queries = nil
41
51
  @enums = nil
42
- @types = nil
52
+ @resources = nil
43
53
  ::HQ::GraphQL::Inputs.reset!
44
54
  ::HQ::GraphQL::Types.reset!
45
55
  end
@@ -52,21 +62,21 @@ module HQ
52
62
  @enums ||= Set.new
53
63
  end
54
64
 
55
- def self.types
56
- @types ||= Set.new
65
+ def self.resources
66
+ @resources ||= Set.new
57
67
  end
58
68
  end
59
69
  end
60
70
 
61
- require "hq/graphql/active_record_extensions"
71
+ require "hq/graphql/association_loader"
62
72
  require "hq/graphql/scalars"
63
73
  require "hq/graphql/comparator"
64
74
  require "hq/graphql/enum"
65
75
  require "hq/graphql/inputs"
66
76
  require "hq/graphql/input_object"
67
- require "hq/graphql/loaders"
68
77
  require "hq/graphql/mutation"
69
78
  require "hq/graphql/object"
79
+ require "hq/graphql/paginated_association_loader"
70
80
  require "hq/graphql/resource"
71
81
  require "hq/graphql/root_mutation"
72
82
  require "hq/graphql/root_query"
@@ -27,7 +27,7 @@ module HQ
27
27
  end
28
28
 
29
29
  def lazy_load!
30
- lazy_load.map(&:call)
30
+ lazy_load.each(&:call)
31
31
  @lazy_load = []
32
32
  end
33
33
 
@@ -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
@@ -5,9 +5,11 @@ module HQ
5
5
  class Config < Struct.new(
6
6
  :authorize,
7
7
  :authorize_field,
8
+ :default_object_class,
8
9
  :default_scope,
9
10
  :extract_class,
10
11
  :resource_lookup,
12
+ :use_experimental_associations,
11
13
  keyword_init: true
12
14
  )
13
15
  def initialize(
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "hq/graphql/types"
4
+
3
5
  module HQ::GraphQL
4
6
  class Enum < ::GraphQL::Schema::Enum
5
7
  ## Auto generate enums from the database using ActiveRecord
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "hq/graphql/enum"
4
+
5
+ module HQ
6
+ class GraphQL::Enum::SortBy < ::HQ::GraphQL::Enum
7
+ value "CreatedAt", value: :created_at
8
+ value "UpdatedAt", value: :updated_at
9
+ end
10
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "hq/graphql/enum"
4
+
5
+ module HQ
6
+ class GraphQL::Enum::SortOrder < ::HQ::GraphQL::Enum
7
+ value "ASC", value: :asc
8
+ value "DESC", value: :desc
9
+ end
10
+ end
@@ -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, :klass
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
- @klass = klass
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 resolve_field(object, args, ctx)
22
- if klass.present? && !!::GraphQL::Batch::Executor.current && object.object
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
@@ -1,5 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "hq/graphql/active_record_extensions"
4
+ require "hq/graphql/inputs"
5
+ require "hq/graphql/types"
6
+
3
7
  module HQ
4
8
  module GraphQL
5
9
  class InputObject < ::GraphQL::Schema::InputObject
@@ -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
- type = find_type(klass)
27
+ resource = ::HQ::GraphQL.lookup_resource(klass)
28
28
 
29
- raise(Error, Error::MISSING_TYPE_MSG % { klass: klass.name }) if !type
30
- type.input_klass
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
@@ -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 ::HQ::GraphQL::ActiveRecordExtensions
15
+ include ActiveRecordExtensions
16
+ extend ObjectAssociation
8
17
 
9
- field_class ::HQ::GraphQL::Field
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,33 @@ module HQ
46
62
  @authorized_action ||= :read
47
63
  end
48
64
 
49
- def field_from_association(association, auto_nil:)
50
- type = ::HQ::GraphQL::Types[association.klass]
51
- name = association.name
65
+ def field_from_association(association, auto_nil:, internal_association: false, &block)
66
+ association_klass = association.klass
67
+ name = association.name
68
+ klass = model_klass
69
+ type = Types[association_klass]
52
70
  case association.macro
53
71
  when :has_many
54
- field name, [type], null: false, klass: model_name
72
+ field name, [type], null: false, klass: model_name do
73
+ if ::HQ::GraphQL.use_experimental_associations?
74
+ extension FieldExtension::PaginatedArguments, klass: association_klass
75
+ extension FieldExtension::PaginatedLoader, klass: klass, association: name, internal_association: internal_association
76
+ else
77
+ extension FieldExtension::AssociationLoaderExtension, klass: klass
78
+ end
79
+ instance_eval(&block) if block
80
+ end
55
81
  else
56
- field name, type, null: !auto_nil || !association_required?(association), klass: model_name
82
+ field name, type, null: !auto_nil || !association_required?(association), klass: model_name do
83
+ extension FieldExtension::AssociationLoaderExtension, klass: klass
84
+ end
57
85
  end
58
- rescue ::HQ::GraphQL::Types::Error
86
+ rescue Types::Error
59
87
  nil
60
88
  end
61
89
 
62
90
  def field_from_column(column, auto_nil:)
63
- field column.name, ::HQ::GraphQL::Types.type_from_column(column), null: !auto_nil || column.null
91
+ field column.name, Types.type_from_column(column), null: !auto_nil || column.null
64
92
  end
65
93
 
66
94
  def association_required?(association)
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HQ
4
+ module GraphQL
5
+ module ObjectAssociation
6
+ def reflect_on_association(association)
7
+ resource_reflections[association.to_s]&.reflection(model_klass)
8
+ end
9
+
10
+ def update(name, &block)
11
+ resource_reflections[name.to_s] = UpdatedReflection.new(name, block)
12
+ end
13
+
14
+ def belongs_to(name, scope = nil, **options, &block)
15
+ add_reflection(name, scope, options, :belongs_to, block)
16
+ end
17
+
18
+ def has_many(name, scope = nil, through: nil, **options, &block)
19
+ raise TypeError, "has_many through is unsupported" if through
20
+ add_reflection(name, scope, options, :has_many, block)
21
+ end
22
+
23
+ private
24
+
25
+ def resource_reflections
26
+ @resource_reflections ||= {}
27
+ end
28
+
29
+ def add_reflection(name, scope, options, macro, block)
30
+ resource_reflections[name.to_s] = ResourceReflection.new(name, scope, options, macro, block)
31
+ end
32
+
33
+ class ResourceReflection
34
+ attr_reader :name, :scope, :options, :macro, :block
35
+
36
+ def initialize(name, scope, options, macro, block)
37
+ @name = name
38
+ @scope = scope
39
+ @options = options
40
+ @macro = macro
41
+ @block = block
42
+ end
43
+
44
+ def reflection(model_klass)
45
+ if macro == :has_many
46
+ ::ActiveRecord::Associations::Builder::HasMany.create_reflection(model_klass, name, scope, options)
47
+ elsif macro == :belongs_to
48
+ ::ActiveRecord::Associations::Builder::BelongsTo.create_reflection(model_klass, name, scope, options)
49
+ end
50
+ end
51
+ end
52
+
53
+ class UpdatedReflection
54
+ attr_reader :name, :block
55
+
56
+ def initialize(name, block)
57
+ @name = name
58
+ @block = block
59
+ end
60
+
61
+ def reflection(model_klass)
62
+ model_klass.reflect_on_association(name)
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end