jpie 3.8.2 → 3.8.3
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/Gemfile.lock +1 -1
- data/README.md +7 -0
- data/lib/json_api/controllers/concerns/resource_actions/include_preloading.rb +1 -1
- data/lib/json_api/controllers/concerns/resource_actions/serialization.rb +7 -7
- data/lib/json_api/resources/resource_loader.rb +30 -8
- data/lib/json_api/serialization/concerns/includes_serialization.rb +11 -4
- data/lib/json_api/serialization/concerns/links_serialization.rb +1 -1
- data/lib/json_api/serialization/concerns/relationships_serialization.rb +1 -0
- data/lib/json_api/serialization/include_filter_cache.rb +3 -2
- data/lib/json_api/serialization/serializer.rb +10 -4
- data/lib/json_api/support/relationship_helpers.rb +3 -1
- data/lib/json_api/support/resource_identifier.rb +2 -2
- data/lib/json_api/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5fee8d7ae2ce0016c8c6b1d0740cf9448d7b932c9d71fc0f74a553ae5bd15d17
|
|
4
|
+
data.tar.gz: 8d8151d6323ad4c3538abb0bb95f23347162d047006af7c9e7d03da638d4f955
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e8e86875e6b6c582640e9f7c03355a8dd271ecab3c1e96186a304f0927b28cccda7fb5009f013b37bf0bd5c778c280e29a03b765033c2e7fe1babba2bd24576a
|
|
7
|
+
data.tar.gz: 44feac1f5b20d4cb6c95f05773e06fefedb7cea9b254fb55a98af26a0b58f5dec60dae425f55bda8c10185f57099c7bd2450729a940c7a31eab8636acbdc1348
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
|
@@ -875,6 +875,13 @@ This will:
|
|
|
875
875
|
- Load `API::V1::WidgetResource` (falls back to `WidgetResource` if `namespace_fallback: true`)
|
|
876
876
|
- Pass `jsonapi_namespace: "api/v1"` to the controller
|
|
877
877
|
|
|
878
|
+
The request namespace also selects the resource class during serialization: for
|
|
879
|
+
the attributes, the include filter, the links, and the relationship linkage.
|
|
880
|
+
`GET /portal/posts` serializes with `Portal::PostResource` even when the `Post`
|
|
881
|
+
model is flat. When the namespaced resource does not exist, the loader falls
|
|
882
|
+
back to the model's own namespace, then to the flat resource. Set
|
|
883
|
+
`config.namespace_fallback = false` to refuse the flat fallback.
|
|
884
|
+
|
|
878
885
|
#### Namespaced Resources
|
|
879
886
|
|
|
880
887
|
Define resources within namespaces that match your route structure:
|
|
@@ -147,7 +147,7 @@ module JSONAPI
|
|
|
147
147
|
return if records.empty?
|
|
148
148
|
|
|
149
149
|
klass = records.first.class
|
|
150
|
-
resource_scope = ResourceLoader.find_for_model(klass).records
|
|
150
|
+
resource_scope = ResourceLoader.find_for_model(klass, namespace: jsonapi_namespace).records
|
|
151
151
|
preload_values = resource_scope.preload_values + resource_scope.includes_values
|
|
152
152
|
return if preload_values.empty?
|
|
153
153
|
|
|
@@ -9,11 +9,9 @@ module JSONAPI
|
|
|
9
9
|
def serialize_resource(resource)
|
|
10
10
|
includes = parse_include_param
|
|
11
11
|
cache = build_include_filter_cache([resource], includes)
|
|
12
|
-
JSONAPI::Serializer.new(resource, authorization_context: self, include_filter_cache: cache
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
document_meta: jsonapi_document_meta,
|
|
16
|
-
)
|
|
12
|
+
serializer = JSONAPI::Serializer.new(resource, authorization_context: self, include_filter_cache: cache,
|
|
13
|
+
namespace: jsonapi_namespace,)
|
|
14
|
+
serializer.to_hash(include: includes, fields: parse_fields_param, document_meta: jsonapi_document_meta)
|
|
17
15
|
end
|
|
18
16
|
|
|
19
17
|
def serialize_collection(resources)
|
|
@@ -48,7 +46,8 @@ module JSONAPI
|
|
|
48
46
|
end
|
|
49
47
|
|
|
50
48
|
def serialize_single(resource, includes, fields, include_context = nil, cache = nil)
|
|
51
|
-
JSONAPI::Serializer.new(resource, authorization_context: self, include_filter_cache: cache
|
|
49
|
+
JSONAPI::Serializer.new(resource, authorization_context: self, include_filter_cache: cache,
|
|
50
|
+
namespace: jsonapi_namespace,)
|
|
52
51
|
.to_hash(include: includes, fields:, document_meta: nil, include_context:)
|
|
53
52
|
end
|
|
54
53
|
|
|
@@ -56,7 +55,8 @@ module JSONAPI
|
|
|
56
55
|
# every included record gets its scope verdict in one query per class,
|
|
57
56
|
# and the serializers answer their per-visit checks from memory.
|
|
58
57
|
def build_include_filter_cache(resources, includes)
|
|
59
|
-
cache = JSONAPI::Serialization::IncludeFilterCache.new(authorization_context: self
|
|
58
|
+
cache = JSONAPI::Serialization::IncludeFilterCache.new(authorization_context: self,
|
|
59
|
+
namespace: jsonapi_namespace,)
|
|
60
60
|
includes.each do |path|
|
|
61
61
|
current = resources
|
|
62
62
|
path.split(".").each do |part|
|
|
@@ -59,20 +59,42 @@ module JSONAPI
|
|
|
59
59
|
def self.find_for_model(model_class, namespace: nil)
|
|
60
60
|
return ActiveStorageBlobResource if active_storage_blob?(model_class)
|
|
61
61
|
|
|
62
|
-
|
|
63
|
-
# @model_cache with these keys to pin STI subclass resources.
|
|
64
|
-
key = namespace ? "#{namespace}|::#{model_class.name}" : "::#{model_class.name}"
|
|
62
|
+
key = model_cache_key(model_class, namespace)
|
|
65
63
|
cached = @model_cache[key]
|
|
66
64
|
return cached if cached
|
|
67
65
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
klass = resolve(candidates) ||
|
|
72
|
-
raise(MissingResourceClass.new(model_class.name, namespace: effective_namespace))
|
|
66
|
+
model_namespace = extract_namespace_from_model(model_class)
|
|
67
|
+
klass = resolve(find_for_model_candidates(model_class, namespace, model_namespace)) ||
|
|
68
|
+
raise(MissingResourceClass.new(model_class.name, namespace: namespace || model_namespace))
|
|
73
69
|
@model_cache[key] = klass
|
|
74
70
|
end
|
|
75
71
|
|
|
72
|
+
def self.find_for_model_candidates(model_class, namespace, model_namespace)
|
|
73
|
+
return model_candidates(model_class, model_namespace) unless namespace
|
|
74
|
+
|
|
75
|
+
request_namespace_candidates(model_class, namespace, model_namespace)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Key format "::ModelName" is public-ish: applications pre-populate
|
|
79
|
+
# @model_cache with these keys to pin STI subclass resources.
|
|
80
|
+
def self.model_cache_key(model_class, namespace)
|
|
81
|
+
return "::#{model_class.name}" unless namespace
|
|
82
|
+
|
|
83
|
+
"#{namespace}|::#{model_class.name}|#{JSONAPI.configuration.namespace_fallback ? 1 : 0}"
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# The request namespace outranks the model's own namespace, which stays a
|
|
87
|
+
# fallback so a namespaced model still reaches its own resource. The flat
|
|
88
|
+
# name comes last, and only while the configuration allows the fallback.
|
|
89
|
+
def self.request_namespace_candidates(model_class, namespace, model_namespace)
|
|
90
|
+
resource_type = model_class.name.demodulize.underscore.pluralize
|
|
91
|
+
candidates = [build_resource_class_name(resource_type, namespace)]
|
|
92
|
+
candidates |= model_candidates(model_class, model_namespace)
|
|
93
|
+
return candidates if JSONAPI.configuration.namespace_fallback
|
|
94
|
+
|
|
95
|
+
candidates - [build_resource_class_name(resource_type, nil)]
|
|
96
|
+
end
|
|
97
|
+
|
|
76
98
|
def self.build_resource_class_name(resource_type, namespace)
|
|
77
99
|
base = "#{resource_type.singularize.classify}Resource"
|
|
78
100
|
return base unless namespace.present?
|
|
@@ -61,7 +61,7 @@ module JSONAPI
|
|
|
61
61
|
end
|
|
62
62
|
|
|
63
63
|
def valid_include_association?(current_record, association_name)
|
|
64
|
-
current_definition = ResourceLoader.find_for_model(current_record.class)
|
|
64
|
+
current_definition = ResourceLoader.find_for_model(current_record.class, namespace:)
|
|
65
65
|
relationship_def = RelationshipHelpers.find_relationship_definition(current_definition, association_name)
|
|
66
66
|
return false unless relationship_def
|
|
67
67
|
return true if self.class.active_storage_attachment?(association_name, current_record.class)
|
|
@@ -91,7 +91,7 @@ module JSONAPI
|
|
|
91
91
|
end
|
|
92
92
|
|
|
93
93
|
def build_scoped_relation(related_klass, association)
|
|
94
|
-
related_base_scope = ResourceLoader.find_for_model(related_klass).records
|
|
94
|
+
related_base_scope = ResourceLoader.find_for_model(related_klass, namespace:).records
|
|
95
95
|
association.scope.merge(apply_include_authorization(related_base_scope, related_klass))
|
|
96
96
|
end
|
|
97
97
|
|
|
@@ -119,12 +119,19 @@ module JSONAPI
|
|
|
119
119
|
return if ctx.processed.include?(build_record_key(related_record))
|
|
120
120
|
|
|
121
121
|
requested = include_paths_to_relationship_names(ctx.all_includes, path_to_record)
|
|
122
|
-
serializer =
|
|
123
|
-
authorization_context:, include_filter_cache:,)
|
|
122
|
+
serializer = child_serializer(related_record, parent_record:, association_name:)
|
|
124
123
|
ctx.included_records << serializer.serialize_record(ctx.fields, requested_relationships: requested)
|
|
125
124
|
ctx.processed.add(build_record_key(related_record))
|
|
126
125
|
end
|
|
127
126
|
|
|
127
|
+
# An included record serializes with the same request context as its
|
|
128
|
+
# parent: the authorization context, the filter cache, and the namespace
|
|
129
|
+
# that selects the resource class.
|
|
130
|
+
def child_serializer(related_record, parent_record:, association_name:)
|
|
131
|
+
self.class.new(related_record, parent_record:, association_name:,
|
|
132
|
+
authorization_context:, include_filter_cache:, namespace:,)
|
|
133
|
+
end
|
|
134
|
+
|
|
128
135
|
def build_record_key(related_record)
|
|
129
136
|
"#{related_record.class.name}-#{related_record.id}"
|
|
130
137
|
end
|
|
@@ -43,7 +43,7 @@ module JSONAPI
|
|
|
43
43
|
end
|
|
44
44
|
|
|
45
45
|
def variant_options_for_parent_association
|
|
46
|
-
parent_definition = ResourceLoader.find_for_model(parent_record.class)
|
|
46
|
+
parent_definition = ResourceLoader.find_for_model(parent_record.class, namespace:)
|
|
47
47
|
rel_def = RelationshipHelpers.find_relationship_definition(parent_definition, association_name)
|
|
48
48
|
rel_def&.dig(:options, :variant)
|
|
49
49
|
end
|
|
@@ -15,8 +15,9 @@ module JSONAPI
|
|
|
15
15
|
Entry = Struct.new(:base_scope, :authorized_scope, :narrowed, :where_clause_empty, :where_hash,
|
|
16
16
|
keyword_init: true,)
|
|
17
17
|
|
|
18
|
-
def initialize(authorization_context: nil)
|
|
18
|
+
def initialize(authorization_context: nil, namespace: nil)
|
|
19
19
|
@authorization_context = authorization_context
|
|
20
|
+
@namespace = namespace
|
|
20
21
|
@entries = {}
|
|
21
22
|
@verdicts = {}
|
|
22
23
|
end
|
|
@@ -53,7 +54,7 @@ module JSONAPI
|
|
|
53
54
|
end
|
|
54
55
|
|
|
55
56
|
def build_entry(klass)
|
|
56
|
-
base_scope = ResourceLoader.find_for_model(klass).records
|
|
57
|
+
base_scope = ResourceLoader.find_for_model(klass, namespace: @namespace).records
|
|
57
58
|
authorized_scope = apply_authorization(base_scope, klass)
|
|
58
59
|
Entry.new(
|
|
59
60
|
base_scope: base_scope,
|
|
@@ -35,10 +35,16 @@ module JSONAPI
|
|
|
35
35
|
@jsonapi_object = nil
|
|
36
36
|
end
|
|
37
37
|
|
|
38
|
+
# namespace is the request's JSON:API namespace. A namespaced endpoint over
|
|
39
|
+
# a flat model (a public portal that re-exposes an internal model) resolves
|
|
40
|
+
# to its own resource only when the namespace reaches this far: resolution
|
|
41
|
+
# by model alone derives the namespace from the model, so a flat model can
|
|
42
|
+
# never reach a namespaced resource.
|
|
38
43
|
def initialize(record, definition: nil, base_definition: nil, parent_record: nil, association_name: nil,
|
|
39
|
-
authorization_context: nil, include_filter_cache: nil)
|
|
44
|
+
authorization_context: nil, include_filter_cache: nil, namespace: nil)
|
|
40
45
|
@record = record
|
|
41
|
-
@
|
|
46
|
+
@namespace = namespace
|
|
47
|
+
@definition = definition || ResourceLoader.find_for_model(record.class, namespace:)
|
|
42
48
|
@base_definition = base_definition
|
|
43
49
|
@parent_record = parent_record
|
|
44
50
|
@association_name = association_name
|
|
@@ -75,12 +81,12 @@ module JSONAPI
|
|
|
75
81
|
|
|
76
82
|
private
|
|
77
83
|
|
|
78
|
-
attr_reader :record, :definition, :parent_record, :association_name, :authorization_context
|
|
84
|
+
attr_reader :record, :definition, :parent_record, :association_name, :authorization_context, :namespace
|
|
79
85
|
|
|
80
86
|
# Shared per request when the controller passes one in; a standalone
|
|
81
87
|
# serializer builds its own, which still dedupes within its own walk.
|
|
82
88
|
def include_filter_cache
|
|
83
|
-
@include_filter_cache ||= Serialization::IncludeFilterCache.new(authorization_context:
|
|
89
|
+
@include_filter_cache ||= Serialization::IncludeFilterCache.new(authorization_context:, namespace:)
|
|
84
90
|
end
|
|
85
91
|
|
|
86
92
|
def base_definition
|
|
@@ -20,7 +20,8 @@ module JSONAPI
|
|
|
20
20
|
# Delegate to ResourceIdentifier
|
|
21
21
|
# rubocop:disable Lint/UnusedMethodArgument
|
|
22
22
|
def serialize_resource_identifier(
|
|
23
|
-
record, association: nil, resource_class: nil, use_instance_class: false, base_resource_class: nil
|
|
23
|
+
record, association: nil, resource_class: nil, use_instance_class: false, base_resource_class: nil,
|
|
24
|
+
namespace: nil
|
|
24
25
|
)
|
|
25
26
|
# rubocop:enable Lint/UnusedMethodArgument
|
|
26
27
|
ResourceIdentifier.serialize_identifier(
|
|
@@ -28,6 +29,7 @@ module JSONAPI
|
|
|
28
29
|
association:,
|
|
29
30
|
definition: resource_class,
|
|
30
31
|
use_instance_class:,
|
|
32
|
+
namespace:,
|
|
31
33
|
)
|
|
32
34
|
end
|
|
33
35
|
|
|
@@ -4,10 +4,10 @@ module JSONAPI
|
|
|
4
4
|
module ResourceIdentifier
|
|
5
5
|
module_function
|
|
6
6
|
|
|
7
|
-
def serialize_identifier(record, association:, definition:, use_instance_class: false)
|
|
7
|
+
def serialize_identifier(record, association:, definition:, use_instance_class: false, namespace: nil)
|
|
8
8
|
model_class = determine_model_class(record, association:, definition:,
|
|
9
9
|
use_instance_class:,)
|
|
10
|
-
related_definition = JSONAPI::ResourceLoader.find_for_model(model_class)
|
|
10
|
+
related_definition = JSONAPI::ResourceLoader.find_for_model(model_class, namespace:)
|
|
11
11
|
related_type = TypeConversion.resource_type_name(related_definition)
|
|
12
12
|
|
|
13
13
|
{ type: related_type, id: record.id.to_s }
|
data/lib/json_api/version.rb
CHANGED