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.
- checksums.yaml +4 -4
- data/.rubocop.yml +1 -0
- data/Gemfile.lock +29 -3
- data/README.md +149 -2
- data/jpie.gemspec +4 -2
- data/lib/json_api/configuration.rb +49 -11
- data/lib/json_api/controllers/concerns/controller_helpers/error_rendering.rb +6 -7
- data/lib/json_api/controllers/concerns/controller_helpers/resource_setup.rb +2 -3
- data/lib/json_api/controllers/concerns/relationships/serialization.rb +42 -1
- data/lib/json_api/controllers/concerns/relationships/updating.rb +1 -5
- data/lib/json_api/controllers/concerns/resource_actions/crud_helpers.rb +0 -8
- data/lib/json_api/controllers/concerns/resource_actions/field_validation.rb +0 -39
- data/lib/json_api/controllers/concerns/resource_actions/filter_validation.rb +3 -7
- data/lib/json_api/controllers/concerns/resource_actions/include_preloading.rb +89 -0
- data/lib/json_api/controllers/concerns/resource_actions/include_validation.rb +50 -0
- data/lib/json_api/controllers/concerns/resource_actions/resource_loading.rb +3 -3
- data/lib/json_api/controllers/concerns/resource_actions/serialization.rb +8 -25
- data/lib/json_api/controllers/concerns/resource_actions.rb +9 -10
- data/lib/json_api/controllers/relationships_controller.rb +2 -3
- data/lib/json_api/errors/parameter_not_allowed.rb +0 -5
- data/lib/json_api/rack/n1_detection.rb +41 -0
- data/lib/json_api/rack/query_tracking.rb +102 -0
- data/lib/json_api/railtie.rb +34 -3
- data/lib/json_api/resources/resource.rb +0 -4
- data/lib/json_api/serialization/concerns/include_filtering.rb +42 -0
- data/lib/json_api/serialization/concerns/includes_serialization.rb +9 -11
- data/lib/json_api/serialization/concerns/links_serialization.rb +12 -4
- data/lib/json_api/serialization/concerns/relationship_processing.rb +1 -5
- data/lib/json_api/support/collection_query.rb +1 -2
- data/lib/json_api/support/concerns/condition_building.rb +5 -15
- data/lib/json_api/support/concerns/nested_filters.rb +1 -1
- data/lib/json_api/support/concerns/polymorphic_filters.rb +1 -1
- data/lib/json_api/support/concerns/regular_filters.rb +3 -21
- data/lib/json_api/support/correlation_id.rb +16 -0
- data/lib/json_api/support/filter_parsing.rb +18 -0
- data/lib/json_api/support/header_warning_subscriber.rb +17 -0
- data/lib/json_api/support/n1_log_subscriber.rb +38 -0
- data/lib/json_api/support/param_helpers.rb +4 -0
- data/lib/json_api/support/prosopite_instrumentation_logger.rb +56 -0
- data/lib/json_api/support/query_tracking_log_subscriber.rb +57 -0
- data/lib/json_api/support/query_tracking_subscriber.rb +76 -0
- data/lib/json_api/support/relationship_guard.rb +1 -1
- data/lib/json_api/support/resource_identifier.rb +2 -1
- data/lib/json_api/support/responders.rb +1 -1
- data/lib/json_api/version.rb +1 -1
- data/lib/json_api.rb +9 -1
- metadata +49 -13
- data/lib/json_api/controllers/concerns/resource_actions/preloading.rb +0 -80
- data/lib/json_api/resources/concerns/eager_load_dsl.rb +0 -50
- data/lib/json_api/resources/concerns/preload_dsl.rb +0 -49
- 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
|
|
@@ -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
|
|
@@ -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
|
-
|
|
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
|
data/lib/json_api/version.rb
CHANGED
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:
|
|
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.
|
|
18
|
+
version: '8.1'
|
|
19
19
|
- - ">="
|
|
20
20
|
- !ruby/object:Gem::Version
|
|
21
|
-
version: 8.
|
|
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.
|
|
28
|
+
version: '8.1'
|
|
29
29
|
- - ">="
|
|
30
30
|
- !ruby/object:Gem::Version
|
|
31
|
-
version: 8.
|
|
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.
|
|
66
|
+
version: '8.1'
|
|
39
67
|
- - ">="
|
|
40
68
|
- !ruby/object:Gem::Version
|
|
41
|
-
version: 8.
|
|
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.
|
|
76
|
+
version: '8.1'
|
|
49
77
|
- - ">="
|
|
50
78
|
- !ruby/object:Gem::Version
|
|
51
|
-
version: 8.
|
|
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
|