blueprinter-activerecord 1.0.2 → 1.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7bec64f8859b0d2cea3346675d4b2a3279245bcb5f7060b0ba0bfcf2f3bb4d83
4
- data.tar.gz: 4fe808fc8be1d043bc18d53dbe3e6a02c40904408ed51f6a8d0fbe0fc1349b2f
3
+ metadata.gz: d4e873acea4991a8ea2761ca16f7564c8dd7ce10b1d0237a7f7fcc7068a64cac
4
+ data.tar.gz: 6398394fe88e9324ae285a9144cb07f1354a9195df196c48c99c011111f113ea
5
5
  SHA512:
6
- metadata.gz: 3f20ed5466369e302e0e8d73bae2bade4fa92cd7efa6ae2bba63eb49494af7990cbc74d371f6c7f4efe782a94ae4d001fbc2dfdbce7a2358771ce6fdc748442c
7
- data.tar.gz: 86b01a869b55e52ac6cfbac55dacb149112a01409ec004d7c55e4ef79451d5716327aa4aa04b8777aef2d68e000498d66f04c813e25d098ad985948bc103ad77
6
+ metadata.gz: c5b87d18310bcd54a677aeec768ca5697f427f267059c20f700def7bc7b386a185577a1f87bd15f87342e76bbe86ece3e472bace0626ad3d869678e64b614a9f
7
+ data.tar.gz: 511f52f12693ce69a8337a74fe7c31cf894d04427a4fa1538ca7570689c0961e8259d559435216c0be1e62417fc93d62f1944e9920c6fcdf4ad56a867f5ae3a2
@@ -42,7 +42,7 @@ module BlueprinterActiveRecord
42
42
  def pre_render(object, blueprint, view, options)
43
43
  if object.is_a?(ActiveRecord::Relation) && object.before_preload_blueprint
44
44
  from_code = object.before_preload_blueprint
45
- from_blueprint = Preloader.preloads(blueprint, view, object.model)
45
+ from_blueprint = Preloader.preloads(blueprint, view, model: object.model)
46
46
  info = PreloadInfo.new(object, from_code, from_blueprint, caller)
47
47
  @log_proc&.call(info)
48
48
  end
@@ -39,7 +39,7 @@ module BlueprinterActiveRecord
39
39
  def pre_render(object, blueprint, view, options)
40
40
  if object.is_a?(ActiveRecord::Relation) && !object.before_preload_blueprint
41
41
  from_code = extract_preloads object
42
- from_blueprint = Preloader.preloads(blueprint, view, object.model)
42
+ from_blueprint = Preloader.preloads(blueprint, view, model: object.model)
43
43
  info = PreloadInfo.new(object, from_code, from_blueprint, caller)
44
44
  @log_proc&.call(info)
45
45
  end
@@ -4,6 +4,7 @@ module BlueprinterActiveRecord
4
4
  # A Blueprinter extension to automatically preload a Blueprint view's ActiveRecord associations during render
5
5
  class Preloader < Blueprinter::Extension
6
6
  include Helpers
7
+ DEFAULT_MAX_RECURSION = 10
7
8
 
8
9
  attr_reader :use, :auto, :auto_proc
9
10
 
@@ -37,11 +38,10 @@ module BlueprinterActiveRecord
37
38
  # intelligently handles them. There are several unit tests which confirm this behavior.
38
39
  #
39
40
  def pre_render(object, blueprint, view, options)
40
- case object.class.name
41
- when "ActiveRecord::Relation", "ActiveRecord::AssociationRelation"
41
+ if object.is_a?(ActiveRecord::Relation) && !object.loaded?
42
42
  if object.preload_blueprint_method || auto || auto_proc&.call(object, blueprint, view, options) == true
43
43
  object.before_preload_blueprint = extract_preloads object
44
- blueprint_preloads = self.class.preloads(blueprint, view, object.model)
44
+ blueprint_preloads = self.class.preloads(blueprint, view, model: object.model)
45
45
  loader = object.preload_blueprint_method || use
46
46
  object.public_send(loader, blueprint_preloads)
47
47
  else
@@ -57,25 +57,74 @@ module BlueprinterActiveRecord
57
57
  #
58
58
  # Returns an ActiveRecord preload plan extracted from the Blueprint and view (recursive).
59
59
  #
60
+ # Preloads are found when one of the model's associations matches:
61
+ # 1. A Blueprint association name.
62
+ # 2. A :preload option on a field or association.
63
+ #
60
64
  # Example:
61
65
  #
62
- # preloads = BlueprinterActiveRecord::Preloader.preloads(WidgetBlueprint, :extended, Widget)
66
+ # preloads = BlueprinterActiveRecord::Preloader.preloads(WidgetBlueprint, :extended, model: Widget)
63
67
  # q = Widget.where(...).order(...).preload(preloads)
64
68
  #
65
69
  # @param blueprint [Class] The Blueprint class
66
70
  # @param view_name [Symbol] Name of the view in blueprint
67
- # @param model [Class] The ActiveRecord model class that blueprint represents
71
+ # @param model [Class|:polymorphic] The ActiveRecord model class that blueprint represents
72
+ # @param cycles [Hash<String, Integer>] (internal) Preloading will halt if recursion/cycles gets too high
68
73
  # @return [Hash] A Hash containing preload/eager_load/etc info for ActiveRecord
69
74
  #
70
- def self.preloads(blueprint, view_name, model=nil)
75
+ def self.preloads(blueprint, view_name, model:, cycles: {})
71
76
  view = blueprint.reflections.fetch(view_name)
72
- view.associations.each_with_object({}) { |(_name, assoc), acc|
73
- ref = model ? model.reflections[assoc.name.to_s] : nil
74
- if (ref || model.nil?) && !assoc.blueprint.is_a?(Proc)
75
- ref_model = ref && !(ref.belongs_to? && ref.polymorphic?) ? ref.klass : nil
76
- acc[assoc.name] = preloads(assoc.blueprint, assoc.view, ref_model)
77
+ preload_vals = view.associations.each_with_object({}) { |(_name, assoc), acc|
78
+ # look for a matching association on the model
79
+ if (preload = association_preloads(assoc, model, cycles))
80
+ acc[assoc.name] = preload
81
+ end
82
+
83
+ # look for a :preload option on the association
84
+ if (custom = assoc.options[:preload])
85
+ Helpers.merge_values custom, acc
77
86
  end
78
87
  }
88
+
89
+ # look for a :preload options on fields
90
+ view.fields.each_with_object(preload_vals) { |(_name, field), acc|
91
+ if (custom = field.options[:preload])
92
+ Helpers.merge_values custom, acc
93
+ end
94
+ }
95
+ end
96
+
97
+ def self.association_preloads(assoc, model, cycles)
98
+ max_cycles = assoc.options.fetch(:max_recursion, DEFAULT_MAX_RECURSION)
99
+ if model == :polymorphic
100
+ if assoc.blueprint.is_a? Proc
101
+ {}
102
+ else
103
+ cycles, count = count_cycles(assoc.blueprint, assoc.view, cycles)
104
+ count < max_cycles ? preloads(assoc.blueprint, assoc.view, model: model, cycles: cycles) : {}
105
+ end
106
+ elsif (ref = model.reflections[assoc.name.to_s])
107
+ if assoc.blueprint.is_a? Proc
108
+ {}
109
+ elsif ref.belongs_to? && ref.polymorphic?
110
+ cycles, count = count_cycles(assoc.blueprint, assoc.view, cycles)
111
+ count < max_cycles ? preloads(assoc.blueprint, assoc.view, model: :polymorphic, cycles: cycles) : {}
112
+ else
113
+ cycles, count = count_cycles(assoc.blueprint, assoc.view, cycles)
114
+ count < max_cycles ? preloads(assoc.blueprint, assoc.view, model: ref.klass, cycles: cycles) : {}
115
+ end
116
+ end
117
+ end
118
+
119
+ def self.count_cycles(blueprint, view, cycles)
120
+ id = "#{blueprint.name || blueprint.inspect}/#{view}"
121
+ cycles = cycles.dup
122
+ if cycles[id].nil?
123
+ cycles[id] = 0
124
+ else
125
+ cycles[id] += 1
126
+ end
127
+ return cycles, cycles[id]
79
128
  end
80
129
  end
81
130
  end
@@ -43,7 +43,7 @@ module BlueprinterActiveRecord
43
43
 
44
44
  if blueprint and view
45
45
  # preload right now
46
- preloads = Preloader.preloads(blueprint, view, model)
46
+ preloads = Preloader.preloads(blueprint, view, model: model)
47
47
  public_send(use, preloads)
48
48
  else
49
49
  # preload during render
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module BlueprinterActiveRecord
4
- VERSION = "1.0.2"
4
+ VERSION = "1.2.0"
5
5
  end
@@ -14,7 +14,7 @@ namespace :blueprinter do
14
14
 
15
15
  model = args[:model].constantize
16
16
  blueprint = args[:blueprint].constantize
17
- preloads = BlueprinterActiveRecord::Preloader.preloads(blueprint, args[:view].to_sym, model)
17
+ preloads = BlueprinterActiveRecord::Preloader.preloads(blueprint, args[:view].to_sym, model: model)
18
18
  puts pretty preloads
19
19
  end
20
20
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: blueprinter-activerecord
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.2
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Procore Technologies, Inc.
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-05-30 00:00:00.000000000 Z
11
+ date: 2024-06-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord