jpie 3.8.3 → 3.10.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: 5fee8d7ae2ce0016c8c6b1d0740cf9448d7b932c9d71fc0f74a553ae5bd15d17
4
- data.tar.gz: 8d8151d6323ad4c3538abb0bb95f23347162d047006af7c9e7d03da638d4f955
3
+ metadata.gz: 637098cfaf33f60d61c1669573a007686467f5218a1b20aff8701886b3fa79d5
4
+ data.tar.gz: 95e995bb47caddc5f3ae867b0e4d3aa2b5fb2de9f40225bba36a7edb527b62b0
5
5
  SHA512:
6
- metadata.gz: e8e86875e6b6c582640e9f7c03355a8dd271ecab3c1e96186a304f0927b28cccda7fb5009f013b37bf0bd5c778c280e29a03b765033c2e7fe1babba2bd24576a
7
- data.tar.gz: 44feac1f5b20d4cb6c95f05773e06fefedb7cea9b254fb55a98af26a0b58f5dec60dae425f55bda8c10185f57099c7bd2450729a940c7a31eab8636acbdc1348
6
+ metadata.gz: ccbdca591a2dcd1a6745f72beb4f477777ac96bfbbf0f3973c56bb4d2f7e63ebecc4bd20feab45d345e887a4d2903aa97c2bce3efa30a36f26403d771311d3fb
7
+ data.tar.gz: 3a6f363d40e998ce65c9805ed06fcf7a2fea1b5e2aac13fd14360f0b8250937b9a00302b230e2a2931d32b8991e55ffdfd93c779cc3534f54f7ed06a1e0755bf
@@ -42,6 +42,12 @@ Gather facts before concluding. Prefer:
42
42
 
43
43
  Do not rely on assumptions. Evidence supports or refutes hypotheses.
44
44
 
45
+ **Bind the error to the incident by identity, not by proximity.** An error in the same account, the same minute, the same job class, or the same feature area is NOT evidence that it caused the reported failure. Busy systems fire several unrelated errors in one window. Prove the link with a record identity that both sides share — the job arguments, the record GID, the message id, the request id. Sentry event details carry `job_arguments` and `extra`; open them and read the ids. If no shared id ties the error to the records the user named, you have a co-occurring error, not the cause.
46
+
47
+ Search the failure's exact time window for EVERY error, not just the ones that match your first idea. Then pick the one whose arguments name the user's records. A narrow search that starts from a suspected culprit returns that culprit and hides the real one.
48
+
49
+ **When the user's evidence contradicts your diagnosis, re-run the search.** Do not restate the theory in better words. Their report of what actually happened outranks your reading of the code. Treat the contradiction as a failed hypothesis and go back to evidence gathering.
50
+
45
51
  ## Methodology and verification
46
52
 
47
53
  - **5 Whys:** State the problem; ask "why?" repeatedly until you reach a cause you can fix. **Verify:** Would fixing this prevent the issue? If not, keep investigating.
@@ -55,6 +61,10 @@ Do not rely on assumptions. Evidence supports or refutes hypotheses.
55
61
  | "Let me try this quick fix" | You do not understand the cause |
56
62
  | "Maybe if I add a guard here" | Guessing, not debugging |
57
63
  | "Let me try a few things" | Random changes waste time |
64
+ | "This error is in the same account and the same minute, so it is the one" | Co-occurrence is not causation; match a record id before you believe it |
65
+ | "The related error is in the same job class / feature, close enough" | Same area is not same incident; open the event's arguments and match the ids |
66
+ | "The user says it still fails, let me explain why my fix is right" | Their evidence beat your theory; re-run the search instead of defending |
67
+ | "I found an error that fits my theory, stop searching" | Search the whole time window; a narrow search only confirms what you assumed |
58
68
 
59
69
  When you notice these, stop and run through problem definition, evidence, and one hypothesis at a time.
60
70
 
@@ -20,6 +20,7 @@ Do not recommend changing code just to pass specs, or changing specs just to mak
20
20
  - Review recent changes (git log, options, caller expectations).
21
21
  - Gather evidence (params, include, serializer path, input data).
22
22
  - Trace backwards from the failure to where correct behaviour diverges.
23
+ - Bind each error you find to the reported incident by a shared record id (job arguments, record GID, message id, request id) — same account, same minute, or same feature is proximity, not proof. Search the whole failure window, not only what matches your first idea.
23
24
 
24
25
  ### Phase 2: Pattern analysis
25
26
 
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- jpie (3.8.3)
4
+ jpie (3.10.0)
5
5
  actionpack (~> 8.1, >= 8.1.0)
6
6
  pg_query (>= 4)
7
7
  prosopite (>= 1)
@@ -70,13 +70,16 @@ module JSONAPI
70
70
  end
71
71
 
72
72
  def determine_sti_class
73
- JSONAPI::ResourceLoader.find(jsonapi_type || resource_type).model_class
73
+ JSONAPI::ResourceLoader.find(jsonapi_type || resource_type, namespace: jsonapi_namespace).model_class
74
74
  rescue JSONAPI::ResourceLoader::MissingResourceClass
75
75
  model_class
76
76
  end
77
77
 
78
+ # The namespace decides which resource defines the writable fields. Resolving the
79
+ # type alone returns the flat resource, so a namespaced endpoint over a flat model
80
+ # would accept every field the flat resource allows.
78
81
  def determine_sti_resource_class
79
- JSONAPI::ResourceLoader.find(jsonapi_type || resource_type)
82
+ JSONAPI::ResourceLoader.find(jsonapi_type || resource_type, namespace: jsonapi_namespace)
80
83
  rescue JSONAPI::ResourceLoader::MissingResourceClass
81
84
  @resource_class
82
85
  end
@@ -8,6 +8,11 @@ module JSONAPI
8
8
 
9
9
  def serialize_resource(resource)
10
10
  includes = parse_include_param
11
+ # On show and update the record arrives through `load_resource_record`, which loaded
12
+ # the include tree via `scope_with_includes`. Each included resource's own `records`
13
+ # preloads still have to be applied, or every included record pays them one query at
14
+ # a time. On create the tree is unloaded, so this finds no targets and does nothing.
15
+ preload_included_resource_associations([resource], includes)
11
16
  cache = build_include_filter_cache([resource], includes)
12
17
  serializer = JSONAPI::Serializer.new(resource, authorization_context: self, include_filter_cache: cache,
13
18
  namespace: jsonapi_namespace,)
@@ -111,8 +111,14 @@ module JSONAPI
111
111
  handler.call(controller: authorization_context, scope:, action: :index, model_class:)
112
112
  end
113
113
 
114
+ # An attachment reaches the serializer as loaded blobs, never as a relation, so it
115
+ # takes the same route as any other loaded association: the filter cache applies the
116
+ # configured authorization_scope for ActiveStorage::Blob. Without it an attachment
117
+ # sideload carried no authorization of its own.
114
118
  def get_active_storage_records(current_record, association_name)
115
- JSONAPI::ActiveStorage::Serialization.blobs_for(association_name, current_record)
119
+ blobs = JSONAPI::ActiveStorage::Serialization.blobs_for(association_name, current_record)
120
+
121
+ include_filter_cache.filter(blobs, ::ActiveStorage::Blob)
116
122
  end
117
123
 
118
124
  def serialize_and_process_record(related_record, path_to_record, ctx, parent_record: nil, association_name: nil)
@@ -20,6 +20,23 @@ module JSONAPI
20
20
 
21
21
  private
22
22
 
23
+ # A belongs_to may name a key that does not match the association
24
+ # (belongs_to :author, foreign_key: "user_id"), so ask the reflection.
25
+ def association_foreign_key(association_name, param_name)
26
+ belongs_to_reflection(association_name)&.foreign_key&.to_s || "#{param_name}_id"
27
+ end
28
+
29
+ def association_foreign_type(association_name, param_name)
30
+ belongs_to_reflection(association_name)&.foreign_type&.to_s || "#{param_name}_type"
31
+ end
32
+
33
+ def belongs_to_reflection(association_name)
34
+ return nil unless @model_class
35
+
36
+ association = @model_class.reflect_on_association(association_name.to_sym)
37
+ association if association&.belongs_to?
38
+ end
39
+
23
40
  def normalize_relationship_value(value)
24
41
  ParamHelpers.ensure_object!(value, "relationship")
25
42
  end
@@ -32,8 +49,10 @@ module JSONAPI
32
49
  if active_storage_attachment?(association_name)
33
50
  attrs[association_name.to_s] = nil
34
51
  else
35
- attrs["#{param_name}_id"] = nil
36
- attrs["#{param_name}_type"] = nil if polymorphic_association?(association_name)
52
+ attrs[association_foreign_key(association_name, param_name)] = nil
53
+ if polymorphic_association?(association_name)
54
+ attrs[association_foreign_type(association_name, param_name)] = nil
55
+ end
37
56
  end
38
57
  end
39
58
 
@@ -61,26 +80,29 @@ module JSONAPI
61
80
 
62
81
  def process_to_many_relationship(attrs, association_name, param_name, data)
63
82
  ids = data.map { |r| extract_id(r) }
64
- types = data.map { |r| extract_type(r) }
65
83
 
66
- if types.any? && self.class.active_storage_blob_type?(types.first)
84
+ # An attachment must take the attachment path whether or not the client sends a
85
+ # `type`. JSON:API requires a resource identifier to carry `type` and `id`, so
86
+ # routing on the association itself keeps the spec-compliant payload working
87
+ # instead of falling through to the `<rel>_ids=` setter, which an attachment lacks.
88
+ if active_storage_attachment?(association_name)
67
89
  process_active_storage_attachment(attrs, association_name, ids, singular: false)
68
90
  return
69
91
  end
70
92
 
93
+ types = data.map { |r| extract_type(r) }
71
94
  validate_relationship_type(association_name, types.first) unless polymorphic_association?(association_name)
72
95
  attrs["#{param_name.singularize}_ids"] = ids
73
96
  end
74
97
 
75
98
  def process_to_one_relationship(attrs, association_name, param_name, data)
76
99
  id = extract_id(data)
77
- type = extract_type(data)
78
100
 
79
- if self.class.active_storage_blob_type?(type)
101
+ if active_storage_attachment?(association_name)
80
102
  return process_active_storage_attachment(attrs, association_name, id, singular: true)
81
103
  end
82
104
 
83
- process_regular_to_one_relationship(attrs, association_name, param_name, id, type)
105
+ process_regular_to_one_relationship(attrs, association_name, param_name, id, extract_type(data))
84
106
  end
85
107
 
86
108
  def process_regular_to_one_relationship(attrs, association_name, param_name, id, type)
@@ -93,8 +115,8 @@ module JSONAPI
93
115
 
94
116
  def process_polymorphic_relationship(attrs, association_name, param_name, id, type)
95
117
  class_name = validate_and_get_class_name(type, association_name)
96
- attrs["#{param_name}_id"] = id
97
- attrs["#{param_name}_type"] = class_name
118
+ attrs[association_foreign_key(association_name, param_name)] = id
119
+ attrs[association_foreign_type(association_name, param_name)] = class_name
98
120
  end
99
121
 
100
122
  def validate_and_get_class_name(type, association_name)
@@ -109,7 +131,7 @@ module JSONAPI
109
131
 
110
132
  def process_non_polymorphic_relationship(attrs, association_name, param_name, id, type)
111
133
  validate_relationship_type(association_name, type)
112
- attrs["#{param_name}_id"] = id
134
+ attrs[association_foreign_key(association_name, param_name)] = id
113
135
  end
114
136
  end
115
137
  end
@@ -45,14 +45,19 @@ module JSONAPI
45
45
  relationships[association_name] = result
46
46
  end
47
47
 
48
+ # Linkage carries a type and an id, so reading the association directly
49
+ # leaks the identity of a record the related endpoint denies — the same
50
+ # bypass the `included` section closes, with a narrower payload. Both
51
+ # sections resolve through one authorized read, so they always describe
52
+ # the same set, and the request-scoped filter cache means the second
53
+ # reader vets nothing the first already vetted.
48
54
  def serialize_relationship_data(association)
49
- related = record.public_send(association.name)
55
+ related = get_association_records(record, association.name)
50
56
 
51
- if association.collection?
52
- serialize_collection_relationship(related, association)
53
- elsif related
54
- serialize_single_relationship(related, association)
55
- end
57
+ return serialize_collection_relationship(related, association) if association.collection?
58
+
59
+ first = related.first
60
+ serialize_single_relationship(first, association) if first
56
61
  end
57
62
 
58
63
  def serialize_collection_relationship(related, association)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module JSONAPI
4
- VERSION = "3.8.3"
4
+ VERSION = "3.10.0"
5
5
  end
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: 3.8.3
4
+ version: 3.10.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Emil Kampp
@@ -222,7 +222,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
222
222
  - !ruby/object:Gem::Version
223
223
  version: '0'
224
224
  requirements: []
225
- rubygems_version: 3.6.9
225
+ rubygems_version: 3.7.2
226
226
  specification_version: 4
227
227
  summary: JSON:API compliant Rails gem for producing and consuming JSON:API resources
228
228
  test_files: []