Module: DataloaderRelationProxy

Defined in:
lib/dataloader_relation_proxy.rb,
lib/dataloader_relation_proxy/lazy.rb,
lib/dataloader_relation_proxy/record.rb,
lib/dataloader_relation_proxy/version.rb,
lib/dataloader_relation_proxy/collection.rb,
lib/dataloader_relation_proxy/active_record_object.rb

Overview

Top level namespace for a system that proxies activerecord relationships through GraphQL dataloaders.

Defined Under Namespace

Modules: Lazy Classes: ActiveRecordObject, Collection, Record

Constant Summary collapse

Proxies =

Namespace to house the proxy classes so they are easily addressable

Module.new
Error =
Class.new(StandardError)
DELEGATE_TO_MODEL_METHODS =
%i[
  ===
  ==
  eql?
  equal?
  <=>
  <
  >
  <=
  >=
].freeze
VERSION =
'0.1.0'

Class Method Summary collapse

Class Method Details

.activerecord_belongs_to(klass, reflection) ⇒ Object

Define a getter that works something like an ActiveRecord belongs_to relationship, except using a dataloader.



148
149
150
151
152
153
154
155
156
157
158
159
# File 'lib/dataloader_relation_proxy.rb', line 148

def self.activerecord_belongs_to(klass, reflection)
  klass.define_method(reflection.name) do
    instance = @dataloader.with(
      ActiveRecordObject,
      reflection.klass,
      reflection.association_primary_key
    ).load(@object.send(reflection.foreign_key))
    return nil if instance.nil?

    DataloaderRelationProxy.for(instance.class).new(instance, @dataloader)
  end
end

.activerecord_has_many(klass, reflection) ⇒ Object

Define a getter that works something like an ActiveRecord has_many relationship, except using a dataloader.



163
164
165
166
167
# File 'lib/dataloader_relation_proxy.rb', line 163

def self.activerecord_has_many(klass, reflection)
  klass.define_method(reflection.name) do
    Collection.new(@object.send(reflection.name), @dataloader)
  end
end

.catch_remaining_methods!(klass) ⇒ Object

rubocop:disable Metrics/MethodLength



127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
# File 'lib/dataloader_relation_proxy.rb', line 127

def self.catch_remaining_methods!(klass)
  klass.instance_eval do
    define_method(:method_missing) do |method_name, *args, &block|
      raise NoMethodError, "Missing method '#{method_name}' on #{@object}" unless @object.respond_to?(method_name)

      self.class.instance_eval do
        delegate method_name, to: :@object
      end

      @object.send(method_name, *args, &block)
    end

    define_method(:respond_to_missing?) do |method_name, include_private = false|
      @object.respond_to?(method_name) || super(method_name, include_private)
    end
  end
end

.define_belongs_to_accessors!(klass, model) ⇒ Object

Given an activerecord model and wrapper class, define an accessor on the wrapper class for each belongs_to relationship on the model driven by GraphQL::Datalaoder.

Parameters:



78
79
80
81
82
83
84
85
86
87
88
89
90
91
# File 'lib/dataloader_relation_proxy.rb', line 78

def self.define_belongs_to_accessors!(klass, model)
  return unless model.respond_to?(:reflect_on_all_associations)

  model.reflect_on_all_associations(:belongs_to).each do |reflection|
    # not sure how to handle this yet
    next if reflection.polymorphic?

    self.for(reflection.klass)

    klass.instance_eval do
      DataloaderRelationProxy.activerecord_belongs_to(klass, reflection)
    end
  end
end

.define_for!(model) ⇒ Object

Defines a wrapper class for the provided model. This wrapper creates classes that extend DataloaderRelationProxy::Record and then defines relationship accessors that are efficiently batched by GraphQL::Dataloader.

Parameters:

  • (Class)


39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
# File 'lib/dataloader_relation_proxy.rb', line 39

def self.define_for!(model)
  @defined << model

  # Recursively define the namespace and wrapper class
  klass = model.name.split('::').reduce(Proxies) do |memo, value|
    next memo.const_get(value, false) if memo.const_defined?(value, false)

    memo.const_set(value, Class.new(DataloaderRelationProxy::Record))
  end

  define_belongs_to_accessors!(klass, model)
  define_has_many_accessors!(klass, model)
  delegate_class_methods!(klass, model)
  catch_remaining_methods!(klass)
end

.define_has_many_accessors!(klass, model) ⇒ Object

Given an activerecord model and wrapper class, define an accessor on the wrapper class for each has_many relationship on the model driven by GraphQL::Datalaoder.

Parameters:



100
101
102
103
104
105
106
107
108
109
110
111
# File 'lib/dataloader_relation_proxy.rb', line 100

def self.define_has_many_accessors!(klass, model)
  return unless model.respond_to?(:reflect_on_all_associations)

  model.reflect_on_all_associations(:has_many).each do |reflection|
    # not sure how to handle this yet
    next if reflection.polymorphic?

    klass.instance_eval do
      DataloaderRelationProxy.activerecord_has_many(klass, reflection)
    end
  end
end

.defined_for?(model) ⇒ Boolean

Determine if there is already a wrapper class defined for the given model.

Parameters:

  • (Class)

Returns:

  • (Boolean)


58
59
60
# File 'lib/dataloader_relation_proxy.rb', line 58

def self.defined_for?(model)
  @defined.include?(model)
end

.delegate_class_methods!(klass, model) ⇒ Object

Delegates some class methods to the underlying model class so that instances of this proxy can more readily stand in for a model instance.

Parameters:



118
119
120
121
122
123
124
# File 'lib/dataloader_relation_proxy.rb', line 118

def self.delegate_class_methods!(klass, model)
  DELEGATE_TO_MODEL_METHODS.each do |m|
    klass.define_singleton_method(m) do |other|
      model.send(m, other)
    end
  end
end

.for(model) ⇒ Object

Ensure a wrapper class is defined for the given model and return it.

Parameters:

  • (Class)


65
66
67
68
69
# File 'lib/dataloader_relation_proxy.rb', line 65

def self.for(model)
  define_for!(model) unless defined_for?(model)

  DataloaderRelationProxy::Proxies.const_get(model.name, false)
end