jpie 1.5.1 → 2.0.1

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.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +1 -0
  3. data/Gemfile.lock +29 -3
  4. data/README.md +149 -2
  5. data/jpie.gemspec +4 -2
  6. data/lib/json_api/configuration.rb +49 -11
  7. data/lib/json_api/controllers/concerns/controller_helpers/error_rendering.rb +6 -7
  8. data/lib/json_api/controllers/concerns/controller_helpers/resource_setup.rb +2 -3
  9. data/lib/json_api/controllers/concerns/relationships/serialization.rb +42 -1
  10. data/lib/json_api/controllers/concerns/relationships/updating.rb +1 -5
  11. data/lib/json_api/controllers/concerns/resource_actions/crud_helpers.rb +0 -8
  12. data/lib/json_api/controllers/concerns/resource_actions/field_validation.rb +0 -39
  13. data/lib/json_api/controllers/concerns/resource_actions/filter_validation.rb +3 -7
  14. data/lib/json_api/controllers/concerns/resource_actions/include_preloading.rb +89 -0
  15. data/lib/json_api/controllers/concerns/resource_actions/include_validation.rb +50 -0
  16. data/lib/json_api/controllers/concerns/resource_actions/resource_loading.rb +3 -3
  17. data/lib/json_api/controllers/concerns/resource_actions/serialization.rb +8 -25
  18. data/lib/json_api/controllers/concerns/resource_actions.rb +9 -10
  19. data/lib/json_api/controllers/relationships_controller.rb +2 -3
  20. data/lib/json_api/errors/parameter_not_allowed.rb +0 -5
  21. data/lib/json_api/rack/n1_detection.rb +41 -0
  22. data/lib/json_api/rack/query_tracking.rb +102 -0
  23. data/lib/json_api/railtie.rb +34 -3
  24. data/lib/json_api/resources/resource.rb +0 -4
  25. data/lib/json_api/serialization/concerns/include_filtering.rb +42 -0
  26. data/lib/json_api/serialization/concerns/includes_serialization.rb +9 -11
  27. data/lib/json_api/serialization/concerns/links_serialization.rb +12 -4
  28. data/lib/json_api/serialization/concerns/relationship_processing.rb +1 -5
  29. data/lib/json_api/support/collection_query.rb +1 -2
  30. data/lib/json_api/support/concerns/condition_building.rb +5 -15
  31. data/lib/json_api/support/concerns/nested_filters.rb +1 -1
  32. data/lib/json_api/support/concerns/polymorphic_filters.rb +1 -1
  33. data/lib/json_api/support/concerns/regular_filters.rb +3 -21
  34. data/lib/json_api/support/correlation_id.rb +16 -0
  35. data/lib/json_api/support/filter_parsing.rb +18 -0
  36. data/lib/json_api/support/header_warning_subscriber.rb +17 -0
  37. data/lib/json_api/support/n1_log_subscriber.rb +38 -0
  38. data/lib/json_api/support/param_helpers.rb +4 -0
  39. data/lib/json_api/support/prosopite_instrumentation_logger.rb +56 -0
  40. data/lib/json_api/support/query_tracking_log_subscriber.rb +57 -0
  41. data/lib/json_api/support/query_tracking_subscriber.rb +76 -0
  42. data/lib/json_api/support/relationship_guard.rb +1 -1
  43. data/lib/json_api/support/resource_identifier.rb +2 -1
  44. data/lib/json_api/support/responders.rb +1 -1
  45. data/lib/json_api/version.rb +1 -1
  46. data/lib/json_api.rb +9 -1
  47. metadata +49 -13
  48. data/lib/json_api/controllers/concerns/resource_actions/preloading.rb +0 -80
  49. data/lib/json_api/resources/concerns/eager_load_dsl.rb +0 -50
  50. data/lib/json_api/resources/concerns/preload_dsl.rb +0 -49
  51. data/lib/json_api/support/response_helpers.rb +0 -10
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JSONAPI
4
+ module Support
5
+ module CorrelationId
6
+ def self.resolve
7
+ resolver = JSONAPI.configuration.correlation_id_resolver
8
+ return nil unless resolver.respond_to?(:call)
9
+
10
+ resolver.call
11
+ rescue StandardError
12
+ nil
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JSONAPI
4
+ module Support
5
+ module FilterParsing
6
+ module_function
7
+
8
+ def parse_column_filter(filter_name)
9
+ m = filter_name.to_s.match(/\A(.+)_(eq|match|lt|lte|gt|gte)\z/)
10
+ m ? { column: m[1], operator: m[2].to_sym } : nil
11
+ end
12
+
13
+ def empty_filter_value?(filter_value)
14
+ filter_value.respond_to?(:empty?) ? filter_value.empty? : filter_value.nil?
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JSONAPI
4
+ module Support
5
+ class HeaderWarningSubscriber
6
+ HEADER_EVENTS = %w[jpie.n1_detected jpie.slow_query_detected].freeze
7
+
8
+ def emit(event)
9
+ ev = event.is_a?(Hash) ? event : event.to_h
10
+ return unless HEADER_EVENTS.include?(ev[:name])
11
+
12
+ Thread.current[:jpie_warnings] ||= []
13
+ Thread.current[:jpie_warnings] << (ev[:name] == "jpie.n1_detected" ? "n1" : "slow_query")
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JSONAPI
4
+ module Support
5
+ class N1LogSubscriber
6
+ def emit(event)
7
+ ev = event.is_a?(Hash) ? event : event.to_h
8
+ return unless ev[:name] == "jpie.n1_detected"
9
+
10
+ payload = ev[:payload] || {}
11
+ log_warn(build_message(payload), payload[:correlation_id])
12
+ end
13
+
14
+ private
15
+
16
+ def log_warn(msg, cid)
17
+ if cid.present?
18
+ Rails.logger.tagged("JPie", "CID(#{cid})") { Rails.logger.warn(msg) }
19
+ else
20
+ Rails.logger.warn("[JPie] #{msg}")
21
+ end
22
+ end
23
+
24
+ def build_message(payload)
25
+ msg = "N+1 detected #{payload[:method]} #{payload[:path]}"
26
+ if payload[:resource_type].present?
27
+ rt = payload[:resource_type]
28
+ rid = payload[:resource_id]
29
+ resource = rid.present? ? "#{rt}/#{rid}" : rt
30
+ msg += " resource=#{resource}"
31
+ end
32
+ msg += " include=#{payload[:include]}" if payload[:include].present?
33
+ msg += " query=#{payload[:n1_query]}" if payload[:n1_query].present?
34
+ msg
35
+ end
36
+ end
37
+ end
38
+ end
@@ -50,5 +50,9 @@ module JSONAPI
50
50
  def esc(val)
51
51
  CGI.escape(val.to_s)
52
52
  end
53
+
54
+ def empty_array?(data)
55
+ data.is_a?(Array) && data.empty?
56
+ end
53
57
  end
54
58
  end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JSONAPI
4
+ module Support
5
+ class ProsopiteInstrumentationLogger
6
+ def warn(progname = nil, &)
7
+ n1_details = block_given? ? yield : progname.to_s
8
+ return if n1_details.nil? || n1_details.empty?
9
+
10
+ env = Thread.current[:jpie_request_env]
11
+ payload = build_payload(env, n1_details)
12
+
13
+ return unless defined?(Rails) && Rails.respond_to?(:event)
14
+
15
+ Rails.event.tagged("jsonapi", "n1") do
16
+ Rails.event.notify("jpie.n1_detected", payload)
17
+ end
18
+ end
19
+
20
+ private
21
+
22
+ def build_payload(env, n1_details)
23
+ base = { n1_details: n1_details, n1_query: extract_first_query(n1_details) }
24
+ result = if env.nil?
25
+ base
26
+ else
27
+ request_payload(ActionDispatch::Request.new(env), n1_details).merge(n1_query: base[:n1_query])
28
+ end
29
+ result.merge(correlation_id: JSONAPI::Support::CorrelationId.resolve)
30
+ end
31
+
32
+ def extract_first_query(n1_details)
33
+ return nil if n1_details.nil? || n1_details.empty?
34
+
35
+ parts = n1_details.split("Call stack:")
36
+ return nil if parts.size < 2
37
+
38
+ query_section = parts[0].sub(/\AN\+1 queries detected:\n?/, "")
39
+ first_line = query_section.each_line.map(&:strip).reject(&:empty?).first
40
+ first_line.presence
41
+ end
42
+
43
+ def request_payload(req, n1_details)
44
+ params = req.params
45
+ {
46
+ path: req.path,
47
+ method: req.request_method,
48
+ include: params[:include],
49
+ resource_type: params[:resource_type],
50
+ resource_id: params[:id],
51
+ n1_details: n1_details,
52
+ }
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JSONAPI
4
+ module Support
5
+ class QueryTrackingLogSubscriber
6
+ QUERY_TRACKING_EVENTS = %w[jpie.slow_query_detected jpie.excessive_queries_detected].freeze
7
+
8
+ def emit(event)
9
+ ev = event.is_a?(Hash) ? event : event.to_h
10
+ name = ev[:name]
11
+ return unless QUERY_TRACKING_EVENTS.include?(name)
12
+
13
+ payload = ev[:payload] || {}
14
+ msg = if name == "jpie.slow_query_detected"
15
+ build_slow_query_message(payload)
16
+ else
17
+ build_excessive_queries_message(payload)
18
+ end
19
+ log_warn(msg, payload[:correlation_id])
20
+ end
21
+
22
+ private
23
+
24
+ def log_warn(msg, cid)
25
+ if cid.present?
26
+ Rails.logger.tagged("JPie", "CID(#{cid})") { Rails.logger.warn(msg) }
27
+ else
28
+ Rails.logger.warn("[JPie] #{msg}")
29
+ end
30
+ end
31
+
32
+ def build_slow_query_message(payload)
33
+ msg = "Slow query detected #{payload[:method]} #{payload[:path]}"
34
+ msg += " resource=#{resource_str(payload)}" if payload[:resource_type].present?
35
+ msg += " include=#{payload[:include]}" if payload[:include].present?
36
+ msg += " duration_ms=#{payload[:duration_ms]}" if payload[:duration_ms].present?
37
+ msg += " sql=#{payload[:sql]}" if payload[:sql].present?
38
+ msg
39
+ end
40
+
41
+ def build_excessive_queries_message(payload)
42
+ msg = "Excessive queries detected #{payload[:method]} #{payload[:path]}"
43
+ msg += " resource=#{resource_str(payload)}" if payload[:resource_type].present?
44
+ msg += " include=#{payload[:include]}" if payload[:include].present?
45
+ msg += " query_count=#{payload[:query_count]}" if payload[:query_count].present?
46
+ msg += " queries=#{payload[:queries].inspect}" if payload[:queries].present?
47
+ msg
48
+ end
49
+
50
+ def resource_str(payload)
51
+ rt = payload[:resource_type]
52
+ rid = payload[:resource_id]
53
+ rid.present? ? "#{rt}/#{rid}" : rt
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JSONAPI
4
+ module Support
5
+ class QueryTrackingSubscriber
6
+ SKIP_PATTERNS = [
7
+ /\A\s*(BEGIN|COMMIT|ROLLBACK|SAVEPOINT|RELEASE\s+SAVEPOINT)\b/i,
8
+ /\A\s*(CREATE|ALTER|DROP)\s+(TABLE|INDEX|DATABASE)/i,
9
+ /\APRAGMA\b/i,
10
+ /sqlite_master|sqlite_temp_master/i,
11
+ /\bFROM\s+pg_/i,
12
+ /\A\s*(SET|SHOW)\s/i,
13
+ ].freeze
14
+
15
+ def call(name, start, finish, id, payload)
16
+ tracking = Thread.current[:jpie_query_tracking]
17
+ return if tracking.nil?
18
+
19
+ sql = payload[:sql]
20
+ return unless count_query?(sql)
21
+
22
+ tracking[:count] += 1
23
+ tracking[:queries] << sql
24
+
25
+ maybe_emit_slow_query(tracking, sql, name, start, finish, id, payload)
26
+ end
27
+
28
+ private
29
+
30
+ def count_query?(sql)
31
+ return false if sql.nil?
32
+
33
+ SKIP_PATTERNS.none? { |pattern| pattern.match?(sql) }
34
+ end
35
+
36
+ def maybe_emit_slow_query(tracking, sql, name, start, finish, id, payload) # rubocop:disable Metrics/ParameterLists
37
+ return unless defined?(Rails) && Rails.respond_to?(:event)
38
+
39
+ duration_ms = ActiveSupport::Notifications::Event.new(name, start, finish, id, payload).duration
40
+ threshold_ms = JSONAPI.configuration.slow_query_threshold_ms
41
+ return if duration_ms < threshold_ms
42
+
43
+ emit_slow_query_detected(tracking, sql, duration_ms, threshold_ms)
44
+ end
45
+
46
+ def emit_slow_query_detected(tracking, sql, duration_ms, threshold_ms)
47
+ event_payload = build_payload(tracking[:env]).merge(
48
+ sql: sql,
49
+ duration_ms: duration_ms,
50
+ threshold_ms: threshold_ms,
51
+ )
52
+
53
+ Rails.event.tagged("jsonapi", "slow_query") do
54
+ Rails.event.notify("jpie.slow_query_detected", event_payload)
55
+ end
56
+ end
57
+
58
+ def build_payload(env)
59
+ base = env.nil? ? {} : request_payload_from_env(env)
60
+ base.merge(correlation_id: JSONAPI::Support::CorrelationId.resolve)
61
+ end
62
+
63
+ def request_payload_from_env(env)
64
+ req = ActionDispatch::Request.new(env)
65
+ params = req.params
66
+ {
67
+ path: req.path,
68
+ method: req.request_method,
69
+ include: params[:include],
70
+ resource_type: params[:resource_type],
71
+ resource_id: params[:id],
72
+ }
73
+ end
74
+ end
75
+ end
76
+ end
@@ -10,7 +10,7 @@ module JSONAPI
10
10
  return unless association
11
11
  return unless readonly
12
12
 
13
- raise JSONAPI::Exceptions::ParameterNotAllowed, [error_target]
13
+ raise JSONAPI::Errors::ParameterNotAllowed, [error_target]
14
14
  end
15
15
  end
16
16
  end
@@ -58,7 +58,8 @@ module JSONAPI
58
58
 
59
59
  def find_related_record(type, id, association, is_polymorphic)
60
60
  related_model_class = resolve_related_model_class(type, association, is_polymorphic)
61
- related_model_class.find(id)
61
+ resource_class = JSONAPI::ResourceLoader.find_for_model(related_model_class)
62
+ resource_class.records.find(id)
62
63
  rescue ActiveRecord::RecordNotFound
63
64
  raise ArgumentError, "Related resource not found: #{type} with id #{id}"
64
65
  rescue NameError
@@ -94,7 +94,7 @@ module JSONAPI
94
94
  def status_code_for(status)
95
95
  return status if status.is_a?(String) && status.match?(/\A\d+\z/)
96
96
 
97
- Rack::Utils::SYMBOL_TO_STATUS_CODE.fetch(status, status).to_s
97
+ ::Rack::Utils::SYMBOL_TO_STATUS_CODE.fetch(status, status).to_s
98
98
  end
99
99
  end
100
100
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module JSONAPI
4
- VERSION = "1.5.1"
4
+ VERSION = "2.0.1"
5
5
  end
data/lib/json_api.rb CHANGED
@@ -29,17 +29,25 @@ require "json_api/support/sort_parsing"
29
29
  require "json_api/support/resource_identifier"
30
30
  require "json_api/support/relationship_helpers"
31
31
  require "json_api/support/param_helpers"
32
+ require "json_api/support/correlation_id"
32
33
  require "json_api/active_storage/detection"
33
34
  require "json_api/active_storage/serialization"
34
35
  require "json_api/active_storage/deserialization"
35
36
  require "json_api/support/active_storage_support"
37
+ require "json_api/support/filter_parsing"
36
38
  require "json_api/support/collection_query"
39
+ require "json_api/support/prosopite_instrumentation_logger"
40
+ require "json_api/support/n1_log_subscriber"
41
+ require "json_api/support/header_warning_subscriber"
42
+ require "json_api/rack/n1_detection"
43
+ require "json_api/support/query_tracking_subscriber"
44
+ require "json_api/support/query_tracking_log_subscriber"
45
+ require "json_api/rack/query_tracking"
37
46
  require "json_api/routing"
38
47
  require "json_api/support/responders"
39
48
  require "json_api/support/instrumentation"
40
49
  require "json_api/serialization/serializer"
41
50
  require "json_api/serialization/deserializer"
42
- require "json_api/support/response_helpers"
43
51
  require "json_api/controllers/concerns/controller_helpers"
44
52
  require "json_api/controllers/concerns/resource_actions"
45
53
  require "json_api/controllers/base_controller"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jpie
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.5.1
4
+ version: 2.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Emil Kampp
@@ -15,40 +15,68 @@ dependencies:
15
15
  requirements:
16
16
  - - "~>"
17
17
  - !ruby/object:Gem::Version
18
- version: '8.0'
18
+ version: '8.1'
19
19
  - - ">="
20
20
  - !ruby/object:Gem::Version
21
- version: 8.0.0
21
+ version: 8.1.0
22
22
  type: :runtime
23
23
  prerelease: false
24
24
  version_requirements: !ruby/object:Gem::Requirement
25
25
  requirements:
26
26
  - - "~>"
27
27
  - !ruby/object:Gem::Version
28
- version: '8.0'
28
+ version: '8.1'
29
29
  - - ">="
30
30
  - !ruby/object:Gem::Version
31
- version: 8.0.0
31
+ version: 8.1.0
32
+ - !ruby/object:Gem::Dependency
33
+ name: pg_query
34
+ requirement: !ruby/object:Gem::Requirement
35
+ requirements:
36
+ - - ">="
37
+ - !ruby/object:Gem::Version
38
+ version: '4'
39
+ type: :runtime
40
+ prerelease: false
41
+ version_requirements: !ruby/object:Gem::Requirement
42
+ requirements:
43
+ - - ">="
44
+ - !ruby/object:Gem::Version
45
+ version: '4'
46
+ - !ruby/object:Gem::Dependency
47
+ name: prosopite
48
+ requirement: !ruby/object:Gem::Requirement
49
+ requirements:
50
+ - - ">="
51
+ - !ruby/object:Gem::Version
52
+ version: '1'
53
+ type: :runtime
54
+ prerelease: false
55
+ version_requirements: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - ">="
58
+ - !ruby/object:Gem::Version
59
+ version: '1'
32
60
  - !ruby/object:Gem::Dependency
33
61
  name: rails
34
62
  requirement: !ruby/object:Gem::Requirement
35
63
  requirements:
36
64
  - - "~>"
37
65
  - !ruby/object:Gem::Version
38
- version: '8.0'
66
+ version: '8.1'
39
67
  - - ">="
40
68
  - !ruby/object:Gem::Version
41
- version: 8.0.0
69
+ version: 8.1.0
42
70
  type: :runtime
43
71
  prerelease: false
44
72
  version_requirements: !ruby/object:Gem::Requirement
45
73
  requirements:
46
74
  - - "~>"
47
75
  - !ruby/object:Gem::Version
48
- version: '8.0'
76
+ version: '8.1'
49
77
  - - ">="
50
78
  - !ruby/object:Gem::Version
51
- version: 8.0.0
79
+ version: 8.1.0
52
80
  description: A Rails 8+ gem that provides jsonapi_resources routing DSL and generic
53
81
  JSON:API controllers
54
82
  email:
@@ -94,22 +122,23 @@ files:
94
122
  - lib/json_api/controllers/concerns/resource_actions/crud_helpers.rb
95
123
  - lib/json_api/controllers/concerns/resource_actions/field_validation.rb
96
124
  - lib/json_api/controllers/concerns/resource_actions/filter_validation.rb
125
+ - lib/json_api/controllers/concerns/resource_actions/include_preloading.rb
126
+ - lib/json_api/controllers/concerns/resource_actions/include_validation.rb
97
127
  - lib/json_api/controllers/concerns/resource_actions/pagination.rb
98
- - lib/json_api/controllers/concerns/resource_actions/preloading.rb
99
128
  - lib/json_api/controllers/concerns/resource_actions/resource_loading.rb
100
129
  - lib/json_api/controllers/concerns/resource_actions/serialization.rb
101
130
  - lib/json_api/controllers/concerns/resource_actions/type_validation.rb
102
131
  - lib/json_api/controllers/relationships_controller.rb
103
132
  - lib/json_api/controllers/resources_controller.rb
104
133
  - lib/json_api/errors/parameter_not_allowed.rb
134
+ - lib/json_api/rack/n1_detection.rb
135
+ - lib/json_api/rack/query_tracking.rb
105
136
  - lib/json_api/railtie.rb
106
137
  - lib/json_api/resources/active_storage_blob_resource.rb
107
138
  - lib/json_api/resources/concerns/attributes_dsl.rb
108
- - lib/json_api/resources/concerns/eager_load_dsl.rb
109
139
  - lib/json_api/resources/concerns/filters_dsl.rb
110
140
  - lib/json_api/resources/concerns/meta_dsl.rb
111
141
  - lib/json_api/resources/concerns/model_class_helpers.rb
112
- - lib/json_api/resources/concerns/preload_dsl.rb
113
142
  - lib/json_api/resources/concerns/relationships_dsl.rb
114
143
  - lib/json_api/resources/concerns/sortable_fields_dsl.rb
115
144
  - lib/json_api/resources/resource.rb
@@ -118,6 +147,7 @@ files:
118
147
  - lib/json_api/serialization/concerns/attributes_deserialization.rb
119
148
  - lib/json_api/serialization/concerns/attributes_serialization.rb
120
149
  - lib/json_api/serialization/concerns/deserialization_helpers.rb
150
+ - lib/json_api/serialization/concerns/include_filtering.rb
121
151
  - lib/json_api/serialization/concerns/includes_serialization.rb
122
152
  - lib/json_api/serialization/concerns/links_serialization.rb
123
153
  - lib/json_api/serialization/concerns/meta_serialization.rb
@@ -136,13 +166,19 @@ files:
136
166
  - lib/json_api/support/concerns/polymorphic_filters.rb
137
167
  - lib/json_api/support/concerns/regular_filters.rb
138
168
  - lib/json_api/support/concerns/sorting.rb
169
+ - lib/json_api/support/correlation_id.rb
170
+ - lib/json_api/support/filter_parsing.rb
171
+ - lib/json_api/support/header_warning_subscriber.rb
139
172
  - lib/json_api/support/instrumentation.rb
173
+ - lib/json_api/support/n1_log_subscriber.rb
140
174
  - lib/json_api/support/param_helpers.rb
175
+ - lib/json_api/support/prosopite_instrumentation_logger.rb
176
+ - lib/json_api/support/query_tracking_log_subscriber.rb
177
+ - lib/json_api/support/query_tracking_subscriber.rb
141
178
  - lib/json_api/support/relationship_guard.rb
142
179
  - lib/json_api/support/relationship_helpers.rb
143
180
  - lib/json_api/support/resource_identifier.rb
144
181
  - lib/json_api/support/responders.rb
145
- - lib/json_api/support/response_helpers.rb
146
182
  - lib/json_api/support/sort_parsing.rb
147
183
  - lib/json_api/support/type_conversion.rb
148
184
  - lib/json_api/testing.rb
@@ -1,80 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module JSONAPI
4
- module ResourceActions
5
- module Preloading
6
- extend ActiveSupport::Concern
7
-
8
- def preload_includes
9
- includes_hash = build_combined_includes_hash
10
- return if includes_hash.empty?
11
-
12
- preload_resources(includes_hash)
13
- rescue ActiveRecord::RecordNotFound
14
- # Will be handled by set_resource
15
- end
16
-
17
- private
18
-
19
- def build_combined_includes_hash
20
- param_includes = parse_include_param
21
- dsl_includes = resource_eager_load_associations
22
-
23
- combined = dsl_includes.map(&:to_s)
24
- combined.concat(param_includes)
25
- combined.uniq!
26
-
27
- build_includes_hash(combined)
28
- end
29
-
30
- def resource_eager_load_associations
31
- return [] unless resource_class.respond_to?(:eager_load_associations)
32
-
33
- resource_class.eager_load_associations
34
- end
35
-
36
- def build_includes_hash(includes)
37
- includes.each_with_object({}) do |path, hash|
38
- merge_include_path(hash, path)
39
- end
40
- end
41
-
42
- def merge_include_path(hash, path)
43
- parts = path.to_s.split(".")
44
- current = hash
45
-
46
- parts.each_with_index do |part, i|
47
- sym = part.to_sym
48
- current[sym] ||= {}
49
- current = current[sym] if i < parts.length - 1
50
- end
51
- end
52
-
53
- def preload_resources(includes_hash)
54
- filtered = filter_includes_for_preload(includes_hash)
55
-
56
- case action_name
57
- when "index" then @preloaded_resources = preload_collection(filtered)
58
- when "show" then @preloaded_resource = preload_single(filtered)
59
- end
60
- end
61
-
62
- def filter_includes_for_preload(hash)
63
- filtered = filter_active_storage_from_includes(hash, model_class)
64
- filter_polymorphic_from_includes(filtered, model_class)
65
- end
66
-
67
- def preload_collection(includes)
68
- return resource_class.records if includes.empty?
69
-
70
- resource_class.records.includes(includes)
71
- end
72
-
73
- def preload_single(includes)
74
- return resource_class.records.find(params[:id]) if includes.empty?
75
-
76
- resource_class.records.includes(includes).find(params[:id])
77
- end
78
- end
79
- end
80
- end
@@ -1,50 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module JSONAPI
4
- module Resources
5
- module EagerLoadDsl
6
- extend ActiveSupport::Concern
7
-
8
- class_methods do
9
- # Declare associations to always eager-load for this resource
10
- # @param associations [Array<Symbol>] list of association names to eager-load
11
- def eager_load(*associations)
12
- @declared_eager_loads ||= []
13
- @declared_eager_loads.concat(associations.map(&:to_sym))
14
- reset_eager_load_associations!
15
- end
16
-
17
- # Get all eager-load associations including inherited ones
18
- # @return [Array<Symbol>] frozen array of association names
19
- def eager_load_associations
20
- return @eager_load_associations if defined?(@eager_load_associations)
21
-
22
- @eager_load_associations = build_eager_load_associations.freeze
23
- end
24
-
25
- def reset_eager_load_associations!
26
- remove_instance_variable(:@eager_load_associations) if defined?(@eager_load_associations)
27
- end
28
- end
29
-
30
- module EagerLoadHelperMethods
31
- def build_eager_load_associations
32
- own_associations = @declared_eager_loads || []
33
- inherited = inherit_eager_load_associations
34
- (inherited + own_associations).uniq
35
- end
36
-
37
- def inherit_eager_load_associations
38
- return [] unless superclass.respond_to?(:eager_load_associations)
39
- return [] if superclass == JSONAPI::Resource
40
-
41
- superclass.eager_load_associations
42
- end
43
- end
44
-
45
- included do
46
- extend EagerLoadHelperMethods
47
- end
48
- end
49
- end
50
- end
@@ -1,49 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module JSONAPI
4
- module Resources
5
- module PreloadDsl
6
- extend ActiveSupport::Concern
7
-
8
- class_methods do
9
- # Override this method in resource subclasses to preload data before serialization.
10
- # This is called once with all records before serialization starts.
11
- # Use this to batch-load data needed by meta methods to avoid N+1 queries.
12
- #
13
- # @param records [Array<ActiveRecord::Base>] the records that will be serialized
14
- # @param context [Hash] optional context (e.g., current user, request params)
15
- # @return [Hash] a hash of preloaded data keyed by record id
16
- #
17
- # @example
18
- # class WorkstreamResource < ApplicationResource
19
- # def self.preload_for_serialization(records, context = {})
20
- # stats = Preloaders::StatsPreloader.call(records: records)
21
- # { stats: stats }
22
- # end
23
- #
24
- # def meta(...)
25
- # preloaded = self.class.preloaded_data[record.id]
26
- # super.merge(loading: preloaded[:stats])
27
- # end
28
- # end
29
- def preload_for_serialization(_records, _context = {})
30
- # Default implementation does nothing - override in subclasses
31
- {}
32
- end
33
-
34
- # Storage for preloaded data (request-scoped via Thread.current)
35
- def preloaded_data
36
- Thread.current["#{name}_preloaded_data"] ||= {}
37
- end
38
-
39
- def preloaded_data=(data)
40
- Thread.current["#{name}_preloaded_data"] = data
41
- end
42
-
43
- def clear_preloaded_data!
44
- Thread.current["#{name}_preloaded_data"] = nil
45
- end
46
- end
47
- end
48
- end
49
- end