hq-graphql 2.0.10 → 2.1.3

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 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