jpie 2.0.2 → 2.1.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 +4 -4
- data/.claude/skills/nasa-power-of-ten-ruby/SKILL.md +71 -0
- data/.claude/skills/root-cause-analysis/SKILL.md +62 -0
- data/.claude/skills/skills-and-subagents/SKILL.md +54 -0
- data/.cursor/agents/bias-reviewer.md +58 -0
- data/.cursor/agents/lint-format.md +58 -0
- data/.cursor/agents/nasa-power-of-ten-reviewer.md +38 -0
- data/.cursor/agents/systematic-debugging.md +52 -0
- data/Gemfile.lock +1 -1
- data/README.md +114 -0
- data/lib/json_api/controllers/concerns/controller_helpers/error_rendering.rb +18 -0
- data/lib/json_api/controllers/concerns/resource_actions/crud_helpers.rb +17 -0
- data/lib/json_api/controllers/concerns/resource_actions/resource_loading.rb +3 -2
- data/lib/json_api/controllers/concerns/resource_actions/serialization.rb +24 -0
- data/lib/json_api/controllers/concerns/resource_actions/sideposting.rb +97 -0
- data/lib/json_api/controllers/concerns/resource_actions/sideposting_primary_first.rb +63 -0
- data/lib/json_api/controllers/concerns/resource_actions.rb +18 -4
- data/lib/json_api/railtie.rb +2 -0
- data/lib/json_api/resources/resource.rb +4 -0
- data/lib/json_api/resources/resource_loader.rb +10 -21
- data/lib/json_api/routing.rb +2 -2
- data/lib/json_api/serialization/concerns/deserialization_helpers.rb +11 -2
- data/lib/json_api/serialization/concerns/relationship_processing.rb +1 -1
- data/lib/json_api/serialization/concerns/relationships_deserialization.rb +12 -0
- data/lib/json_api/sideposting/lid_resolver.rb +72 -0
- data/lib/json_api/sideposting/order.rb +11 -0
- data/lib/json_api/sideposting/processor.rb +118 -0
- data/lib/json_api/support/relationship_helpers.rb +4 -0
- data/lib/json_api/support/resource_identifier.rb +4 -0
- data/lib/json_api/version.rb +1 -1
- data/lib/json_api.rb +6 -1
- metadata +13 -1
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json_api/sideposting/order"
|
|
4
|
+
require_relative "sideposting_primary_first"
|
|
5
|
+
|
|
6
|
+
module JSONAPI
|
|
7
|
+
module ResourceActions
|
|
8
|
+
module Sideposting
|
|
9
|
+
extend ActiveSupport::Concern
|
|
10
|
+
include SidepostingPrimaryFirst
|
|
11
|
+
|
|
12
|
+
class_methods do
|
|
13
|
+
def sidepost_order(value)
|
|
14
|
+
order = value.to_sym
|
|
15
|
+
unless JSONAPI::Sideposting::Order::VALID_ORDERS.include?(order)
|
|
16
|
+
raise ArgumentError, "sidepost_order must be one of #{JSONAPI::Sideposting::Order::VALID_ORDERS.join(", ")}"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
@sidepost_order = order
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def sidepost_order_value
|
|
23
|
+
@sidepost_order || JSONAPI::Sideposting::Order::PRIMARY_FIRST
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def included_first?
|
|
27
|
+
sidepost_order_value == JSONAPI::Sideposting::Order::INCLUDED_FIRST
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def sidepost_request?
|
|
32
|
+
params[:included].present?
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def create_with_sidepost
|
|
36
|
+
if self.class.included_first?
|
|
37
|
+
create_with_sidepost_included_first
|
|
38
|
+
else
|
|
39
|
+
create_with_sidepost_primary_first
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def create_with_sidepost_included_first
|
|
44
|
+
ActiveRecord::Base.transaction do
|
|
45
|
+
processor = JSONAPI::Sideposting::Processor.new(included: params[:included], controller: self)
|
|
46
|
+
processor.create_all!
|
|
47
|
+
resolved_data = processor.lid_resolver.resolve(raw_jsonapi_data)
|
|
48
|
+
resource = build_resource_from_resolved_data(resolved_data)
|
|
49
|
+
authorize_resource_action!(resource, action: :create)
|
|
50
|
+
attach_active_storage_files(resource, @create_attachments, resource_class: determine_sti_resource_class)
|
|
51
|
+
save_created_with_sidepost(resource, processor.created_records)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def extract_lid_relationships!(data)
|
|
56
|
+
extracted = {}
|
|
57
|
+
rels = data[:relationships] || {}
|
|
58
|
+
rels.each do |key, val|
|
|
59
|
+
next unless val.is_a?(Hash)
|
|
60
|
+
|
|
61
|
+
d = val[:data] || val["data"]
|
|
62
|
+
next unless contains_lid?(d)
|
|
63
|
+
|
|
64
|
+
extracted[key] = val
|
|
65
|
+
end
|
|
66
|
+
extracted.each_key { |k| rels.delete(k) }
|
|
67
|
+
extracted
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def contains_lid?(rel_data)
|
|
71
|
+
return false if rel_data.blank?
|
|
72
|
+
|
|
73
|
+
if rel_data.is_a?(Array)
|
|
74
|
+
rel_data.any? { |item| contains_lid?(item) }
|
|
75
|
+
else
|
|
76
|
+
(rel_data[:lid] || rel_data["lid"]).to_s.present?
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def apply_resolved_relationships!(resource, resolved_relationships)
|
|
81
|
+
payload = { relationships: resolved_relationships }
|
|
82
|
+
params_hash = JSONAPI::Deserializer.new(
|
|
83
|
+
payload,
|
|
84
|
+
model_class: resource.class,
|
|
85
|
+
action: :update,
|
|
86
|
+
).to_model_attributes
|
|
87
|
+
resource.update!(params_hash)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def build_resource_from_resolved_data(resolved_data)
|
|
91
|
+
sti_class = determine_sti_class
|
|
92
|
+
params_hash, @create_attachments = prepare_create_params_from_data(sti_class, resolved_data)
|
|
93
|
+
sti_class.new(params_hash)
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JSONAPI
|
|
4
|
+
module ResourceActions
|
|
5
|
+
module SidepostingPrimaryFirst
|
|
6
|
+
extend ActiveSupport::Concern
|
|
7
|
+
|
|
8
|
+
def create_with_sidepost_primary_first
|
|
9
|
+
ActiveRecord::Base.transaction do
|
|
10
|
+
data = raw_jsonapi_data.dup
|
|
11
|
+
lid_relationships = extract_lid_relationships!(data)
|
|
12
|
+
resource, lid_resolver = create_primary_and_lid_resolver(data)
|
|
13
|
+
processor = run_sidepost_processor(lid_resolver)
|
|
14
|
+
apply_lid_relationships_if_any(resource, lid_relationships, lid_resolver)
|
|
15
|
+
render_created_with_sidepost(resource, processor)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def create_primary_and_lid_resolver(data)
|
|
20
|
+
resource = build_resource_from_resolved_data(data)
|
|
21
|
+
authorize_resource_action!(resource, action: :create)
|
|
22
|
+
attach_active_storage_files(resource, @create_attachments, resource_class: determine_sti_resource_class)
|
|
23
|
+
raise JSONAPI::Sideposting::PrimaryValidationError, resource unless resource.save
|
|
24
|
+
|
|
25
|
+
[resource, build_lid_resolver_with_primary(data, resource)]
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def build_lid_resolver_with_primary(data, resource)
|
|
29
|
+
lid_resolver = JSONAPI::Sideposting::LidResolver.new
|
|
30
|
+
primary_lid = (data[:lid] || data["lid"]).to_s.presence
|
|
31
|
+
if primary_lid
|
|
32
|
+
type_name = RelationshipHelpers.resource_type_name(determine_sti_resource_class)
|
|
33
|
+
lid_resolver.add(primary_lid, type: type_name, id: resource.id)
|
|
34
|
+
end
|
|
35
|
+
lid_resolver
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def run_sidepost_processor(lid_resolver)
|
|
39
|
+
processor = JSONAPI::Sideposting::Processor.new(
|
|
40
|
+
included: params[:included],
|
|
41
|
+
controller: self,
|
|
42
|
+
lid_resolver: lid_resolver,
|
|
43
|
+
)
|
|
44
|
+
processor.create_all!
|
|
45
|
+
processor
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def apply_lid_relationships_if_any(resource, lid_relationships, lid_resolver)
|
|
49
|
+
return if lid_relationships.blank?
|
|
50
|
+
|
|
51
|
+
resolved = lid_resolver.resolve(lid_relationships)
|
|
52
|
+
apply_resolved_relationships!(resource, resolved)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def render_created_with_sidepost(resource, processor)
|
|
56
|
+
emit_resource_event(:created, resource)
|
|
57
|
+
render json: serialize_resource_with_sidepost(resource, processor.created_records),
|
|
58
|
+
status: :created,
|
|
59
|
+
location: resource_url(resource)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -9,6 +9,7 @@ require_relative "resource_actions/pagination"
|
|
|
9
9
|
require_relative "resource_actions/type_validation"
|
|
10
10
|
require_relative "resource_actions/crud_helpers"
|
|
11
11
|
require_relative "resource_actions/resource_loading"
|
|
12
|
+
require_relative "resource_actions/sideposting"
|
|
12
13
|
|
|
13
14
|
module JSONAPI
|
|
14
15
|
module ResourceActions
|
|
@@ -22,6 +23,7 @@ module JSONAPI
|
|
|
22
23
|
include TypeValidation
|
|
23
24
|
include CrudHelpers
|
|
24
25
|
include ResourceLoading
|
|
26
|
+
include Sideposting
|
|
25
27
|
|
|
26
28
|
included do
|
|
27
29
|
before_action :load_jsonapi_resource
|
|
@@ -47,16 +49,28 @@ module JSONAPI
|
|
|
47
49
|
end
|
|
48
50
|
|
|
49
51
|
def create
|
|
50
|
-
|
|
51
|
-
authorize_resource_action!(resource, action: :create)
|
|
52
|
-
attach_active_storage_files(resource, @create_attachments, resource_class: determine_sti_resource_class)
|
|
53
|
-
save_created(resource)
|
|
52
|
+
perform_create
|
|
54
53
|
rescue ArgumentError => e
|
|
55
54
|
render_create_error(e)
|
|
55
|
+
rescue JSONAPI::Sideposting::ValidationError => e
|
|
56
|
+
render_sidepost_validation_errors(e)
|
|
57
|
+
rescue JSONAPI::Sideposting::PrimaryValidationError => e
|
|
58
|
+
render_validation_errors(e.record)
|
|
56
59
|
rescue JSONAPI::Errors::ParameterNotAllowed, ActiveSupport::MessageVerifier::InvalidSignature => e
|
|
57
60
|
handle_create_exception(e)
|
|
58
61
|
end
|
|
59
62
|
|
|
63
|
+
def perform_create
|
|
64
|
+
if sidepost_request?
|
|
65
|
+
create_with_sidepost
|
|
66
|
+
else
|
|
67
|
+
resource = build_resource_for_create
|
|
68
|
+
authorize_resource_action!(resource, action: :create)
|
|
69
|
+
attach_active_storage_files(resource, @create_attachments, resource_class: determine_sti_resource_class)
|
|
70
|
+
save_created(resource)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
60
74
|
def build_resource_for_create
|
|
61
75
|
sti_class = determine_sti_class
|
|
62
76
|
params_hash, @create_attachments = prepare_create_params(sti_class)
|
data/lib/json_api/railtie.rb
CHANGED
|
@@ -132,6 +132,8 @@ module JSONAPI
|
|
|
132
132
|
load "json_api/controllers/base_controller.rb"
|
|
133
133
|
JSONAPI.send(:remove_const, :RelationshipsController)
|
|
134
134
|
load "json_api/controllers/relationships_controller.rb"
|
|
135
|
+
JSONAPI.send(:remove_const, :ResourcesController)
|
|
136
|
+
load "json_api/controllers/resources_controller.rb"
|
|
135
137
|
end
|
|
136
138
|
|
|
137
139
|
def include_jsonapi_concerns(base_controller_class)
|
|
@@ -16,25 +16,7 @@ module JSONAPI
|
|
|
16
16
|
end
|
|
17
17
|
end
|
|
18
18
|
|
|
19
|
-
# Thread-safe caches for resource class lookups
|
|
20
|
-
# Using simple Hash with ||= is safe enough - worst case is duplicate computation
|
|
21
|
-
# which is acceptable since the result is always the same Class object
|
|
22
|
-
@find_cache = {}
|
|
23
|
-
@model_cache = {}
|
|
24
|
-
|
|
25
|
-
class << self
|
|
26
|
-
def clear_cache!
|
|
27
|
-
@find_cache = {}
|
|
28
|
-
@model_cache = {}
|
|
29
|
-
end
|
|
30
|
-
end
|
|
31
|
-
|
|
32
19
|
def self.find(resource_type, namespace: nil)
|
|
33
|
-
cache_key = "#{namespace}::#{resource_type}"
|
|
34
|
-
@find_cache[cache_key] ||= find_uncached(resource_type, namespace:)
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
def self.find_uncached(resource_type, namespace: nil)
|
|
38
20
|
return find_namespaced(resource_type, namespace) if namespace.present?
|
|
39
21
|
|
|
40
22
|
find_flat(resource_type, namespace)
|
|
@@ -66,8 +48,7 @@ module JSONAPI
|
|
|
66
48
|
def self.find_for_model(model_class, namespace: nil)
|
|
67
49
|
return ActiveStorageBlobResource if active_storage_blob?(model_class)
|
|
68
50
|
|
|
69
|
-
|
|
70
|
-
@model_cache[cache_key] ||= find_resource_for_model(model_class, namespace)
|
|
51
|
+
find_resource_for_model(model_class, namespace)
|
|
71
52
|
end
|
|
72
53
|
|
|
73
54
|
def self.find_resource_for_model(model_class, namespace)
|
|
@@ -76,7 +57,15 @@ module JSONAPI
|
|
|
76
57
|
|
|
77
58
|
find(resource_type, namespace: effective_namespace)
|
|
78
59
|
rescue MissingResourceClass
|
|
79
|
-
find_base_class_resource(model_class, effective_namespace)
|
|
60
|
+
try_flat_full_name_resource(model_class) || find_base_class_resource(model_class, effective_namespace)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def self.try_flat_full_name_resource(model_class)
|
|
64
|
+
return nil unless model_class.name.include?("::")
|
|
65
|
+
|
|
66
|
+
"#{model_class.name.gsub("::", "")}Resource".constantize
|
|
67
|
+
rescue NameError
|
|
68
|
+
nil
|
|
80
69
|
end
|
|
81
70
|
|
|
82
71
|
def self.find_base_class_resource(model_class, effective_namespace)
|
data/lib/json_api/routing.rb
CHANGED
|
@@ -85,9 +85,9 @@ module JSONAPI
|
|
|
85
85
|
next if sub_resource_name == resource.to_sym
|
|
86
86
|
|
|
87
87
|
jsonapi_resources(sub_resource_name, defaults:)
|
|
88
|
+
rescue NameError, JSONAPI::ResourceLoader::MissingResourceClass
|
|
89
|
+
next
|
|
88
90
|
end
|
|
89
|
-
rescue NameError, JSONAPI::ResourceLoader::MissingResourceClass
|
|
90
|
-
nil
|
|
91
91
|
end
|
|
92
92
|
end
|
|
93
93
|
end
|
|
@@ -52,6 +52,11 @@ module JSONAPI
|
|
|
52
52
|
|
|
53
53
|
def extract_id(resource_identifier)
|
|
54
54
|
id = RelationshipHelpers.extract_id_from_identifier(resource_identifier)
|
|
55
|
+
if id.nil? && lid?(resource_identifier)
|
|
56
|
+
raise ArgumentError,
|
|
57
|
+
"lid references must be resolved before deserialization " \
|
|
58
|
+
"(include related resources in top-level included)"
|
|
59
|
+
end
|
|
55
60
|
raise ArgumentError, "Missing id in relationship data" unless id
|
|
56
61
|
|
|
57
62
|
id
|
|
@@ -75,11 +80,15 @@ module JSONAPI
|
|
|
75
80
|
def valid_resource_identifier?(identifier)
|
|
76
81
|
return false unless identifier.is_a?(Hash)
|
|
77
82
|
|
|
78
|
-
|
|
83
|
+
has_type?(identifier) && (has_id?(identifier) || lid?(identifier))
|
|
79
84
|
end
|
|
80
85
|
|
|
81
86
|
def has_id?(identifier)
|
|
82
|
-
identifier[:id]
|
|
87
|
+
identifier[:id].present?
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def lid?(identifier)
|
|
91
|
+
identifier[:lid].to_s.present?
|
|
83
92
|
end
|
|
84
93
|
|
|
85
94
|
def has_type?(identifier)
|
|
@@ -47,7 +47,7 @@ module JSONAPI
|
|
|
47
47
|
def validate_relationship_data_format!(data, association_name)
|
|
48
48
|
return if valid_relationship_data?(data)
|
|
49
49
|
|
|
50
|
-
raise ArgumentError, "Invalid relationship data for #{association_name}: missing type or id"
|
|
50
|
+
raise ArgumentError, "Invalid relationship data for #{association_name}: missing type or (id or lid)"
|
|
51
51
|
end
|
|
52
52
|
|
|
53
53
|
def process_relationship_data(attrs, association_name, param_name, data)
|
|
@@ -35,10 +35,22 @@ module JSONAPI
|
|
|
35
35
|
end
|
|
36
36
|
end
|
|
37
37
|
|
|
38
|
+
def extract_identifiers_from_data(data)
|
|
39
|
+
if data.is_a?(Array)
|
|
40
|
+
data.map { |r| r.to_h.symbolize_keys }
|
|
41
|
+
else
|
|
42
|
+
[data.to_h.symbolize_keys]
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
38
46
|
def extract_id_from_identifier(identifier)
|
|
39
47
|
RelationshipHelpers.extract_id_from_identifier(identifier)
|
|
40
48
|
end
|
|
41
49
|
|
|
50
|
+
def extract_lid_from_identifier(identifier)
|
|
51
|
+
RelationshipHelpers.extract_lid_from_identifier(identifier)
|
|
52
|
+
end
|
|
53
|
+
|
|
42
54
|
def relationship_id(relationship_name)
|
|
43
55
|
relationship_ids(relationship_name).first
|
|
44
56
|
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JSONAPI
|
|
4
|
+
module Sideposting
|
|
5
|
+
class LidResolver
|
|
6
|
+
def initialize
|
|
7
|
+
@map = {}
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def add(lid, type:, id:)
|
|
11
|
+
raise ArgumentError, "lid is required" if lid.blank?
|
|
12
|
+
raise ArgumentError, "type is required" if type.blank?
|
|
13
|
+
raise ArgumentError, "id is required" if id.blank?
|
|
14
|
+
|
|
15
|
+
@map[lid.to_s] = { type: type.to_s, id: id.to_s }
|
|
16
|
+
self
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def resolve(data)
|
|
20
|
+
return data if data.nil?
|
|
21
|
+
|
|
22
|
+
case data
|
|
23
|
+
when Hash
|
|
24
|
+
resolve_hash(data)
|
|
25
|
+
when Array
|
|
26
|
+
data.map { |item| resolve(item) }
|
|
27
|
+
else
|
|
28
|
+
data
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def [](lid)
|
|
33
|
+
@map[lid.to_s]
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def resolve_hash(hash)
|
|
39
|
+
result = {}
|
|
40
|
+
hash.each do |key, value|
|
|
41
|
+
result[key] = if resource_identifier_with_lid?(value)
|
|
42
|
+
resolve_identifier(value)
|
|
43
|
+
else
|
|
44
|
+
resolve(value)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
result
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def resource_identifier_with_lid?(value)
|
|
51
|
+
return false unless value.is_a?(Hash)
|
|
52
|
+
|
|
53
|
+
value = value.symbolize_keys if value.respond_to?(:symbolize_keys)
|
|
54
|
+
type = value[:type] || value["type"]
|
|
55
|
+
lid = value[:lid] || value["lid"]
|
|
56
|
+
type.present? && lid.to_s.present?
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def resolve_identifier(identifier)
|
|
60
|
+
identifier = identifier.symbolize_keys if identifier.respond_to?(:symbolize_keys)
|
|
61
|
+
lid = (identifier[:lid] || identifier["lid"]).to_s
|
|
62
|
+
resolved = @map[lid]
|
|
63
|
+
unless resolved
|
|
64
|
+
raise ArgumentError,
|
|
65
|
+
"Unknown lid '#{lid}' (ensure the resource is in the top-level included array)"
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
{ type: resolved[:type], id: resolved[:id] }
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json_api/sideposting/lid_resolver"
|
|
4
|
+
|
|
5
|
+
module JSONAPI
|
|
6
|
+
module Sideposting
|
|
7
|
+
class ValidationError < JSONAPI::Error
|
|
8
|
+
attr_reader :record, :included_index
|
|
9
|
+
|
|
10
|
+
def initialize(record:, included_index:)
|
|
11
|
+
@record = record
|
|
12
|
+
@included_index = included_index
|
|
13
|
+
super("Validation failed for included resource at index #{included_index}")
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
class PrimaryValidationError < JSONAPI::Error
|
|
18
|
+
attr_reader :record
|
|
19
|
+
|
|
20
|
+
def initialize(record)
|
|
21
|
+
@record = record
|
|
22
|
+
super("Validation failed for primary resource")
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
class Processor
|
|
27
|
+
def initialize(included:, controller:, lid_resolver: nil)
|
|
28
|
+
raise ArgumentError, "included is required" if included.blank?
|
|
29
|
+
raise ArgumentError, "controller is required" if controller.nil?
|
|
30
|
+
|
|
31
|
+
@included = Array(included).map { |item| symbolize_item(item) }
|
|
32
|
+
@controller = controller
|
|
33
|
+
@lid_resolver = lid_resolver || LidResolver.new
|
|
34
|
+
@created_records = []
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def create_all!
|
|
38
|
+
@included.each_with_index do |item, index|
|
|
39
|
+
record = create_one(item, index)
|
|
40
|
+
@created_records << record
|
|
41
|
+
lid = item[:lid].to_s.presence
|
|
42
|
+
next unless lid
|
|
43
|
+
|
|
44
|
+
resource_class = resource_class_for(item[:type])
|
|
45
|
+
type_name = RelationshipHelpers.resource_type_name(resource_class)
|
|
46
|
+
@lid_resolver.add(lid, type: type_name, id: record.id)
|
|
47
|
+
end
|
|
48
|
+
@created_records
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
attr_reader :lid_resolver, :created_records
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
def symbolize_item(item)
|
|
56
|
+
item = item.to_unsafe_h if item.respond_to?(:to_unsafe_h)
|
|
57
|
+
ParamHelpers.deep_symbolize_params(item.to_h)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def create_one(item, included_index)
|
|
61
|
+
type = item[:type].to_s.presence
|
|
62
|
+
raise ArgumentError, "Included resource is missing type" unless type
|
|
63
|
+
|
|
64
|
+
resource_class = resource_class_for(type)
|
|
65
|
+
model_class = resource_class.model_class
|
|
66
|
+
params_hash, attachments = prepare_create_params_for_item(item, model_class, resource_class)
|
|
67
|
+
|
|
68
|
+
custom = try_sidepost_create(resource_class, params_hash, included_index)
|
|
69
|
+
return custom if custom
|
|
70
|
+
|
|
71
|
+
create_record_default(model_class, params_hash, attachments, resource_class, included_index)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def try_sidepost_create(resource_class, params_hash, included_index)
|
|
75
|
+
custom = resource_class.sidepost_create(params: params_hash, controller: @controller)
|
|
76
|
+
return nil unless custom
|
|
77
|
+
|
|
78
|
+
raise ValidationError.new(record: custom, included_index: included_index) unless custom.persisted?
|
|
79
|
+
|
|
80
|
+
custom
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def create_record_default(model_class, params_hash, attachments, resource_class, included_index)
|
|
84
|
+
record = model_class.new(params_hash)
|
|
85
|
+
@controller.send(:authorize_resource_action!, record, action: :create)
|
|
86
|
+
@controller.send(:attach_active_storage_files, record, attachments, resource_class: resource_class)
|
|
87
|
+
raise ValidationError.new(record: record, included_index: included_index) unless record.save
|
|
88
|
+
|
|
89
|
+
record
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def resource_class_for(type)
|
|
93
|
+
namespace = @controller.instance_variable_get(:@jsonapi_namespace)
|
|
94
|
+
ResourceLoader.find(type, namespace: namespace)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def prepare_create_params_for_item(item, model_class, _resource_class)
|
|
98
|
+
hash = Deserializer.new(item, model_class: model_class, action: :create).to_model_attributes
|
|
99
|
+
attachments = @controller.send(:extract_active_storage_params_from_hash, hash, model_class)
|
|
100
|
+
attachments.each_key { |k| hash.delete(k.to_s) }
|
|
101
|
+
hash.delete("type")
|
|
102
|
+
hash.delete(:type)
|
|
103
|
+
apply_sti_type(model_class, hash) if sti_base_with_type_column?(model_class)
|
|
104
|
+
[hash, attachments]
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def apply_sti_type(klass, params)
|
|
108
|
+
return unless sti_base_with_type_column?(klass)
|
|
109
|
+
|
|
110
|
+
params["type"] = klass.name
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def sti_base_with_type_column?(klass)
|
|
114
|
+
klass.respond_to?(:base_class) && klass.base_class == klass && klass.column_names.include?("type")
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
@@ -44,6 +44,10 @@ module JSONAPI
|
|
|
44
44
|
ResourceIdentifier.extract_id(identifier)
|
|
45
45
|
end
|
|
46
46
|
|
|
47
|
+
def extract_lid_from_identifier(identifier)
|
|
48
|
+
ResourceIdentifier.extract_lid(identifier)
|
|
49
|
+
end
|
|
50
|
+
|
|
47
51
|
def extract_type_from_identifier(identifier)
|
|
48
52
|
ResourceIdentifier.extract_type(identifier)
|
|
49
53
|
end
|
data/lib/json_api/version.rb
CHANGED
data/lib/json_api.rb
CHANGED
|
@@ -11,7 +11,7 @@ end
|
|
|
11
11
|
require "json_api/errors/parameter_not_allowed"
|
|
12
12
|
|
|
13
13
|
module JSONAPI
|
|
14
|
-
# Rebuild BaseController and
|
|
14
|
+
# Rebuild BaseController, RelationshipsController, and ResourcesController to reflect the current
|
|
15
15
|
# base_controller_class configuration. Safe to call repeatedly.
|
|
16
16
|
def self.rebuild_base_controllers!
|
|
17
17
|
remove_const(:BaseController) if const_defined?(:BaseController)
|
|
@@ -19,6 +19,9 @@ module JSONAPI
|
|
|
19
19
|
|
|
20
20
|
remove_const(:RelationshipsController) if const_defined?(:RelationshipsController)
|
|
21
21
|
load "json_api/controllers/relationships_controller.rb"
|
|
22
|
+
|
|
23
|
+
remove_const(:ResourcesController) if const_defined?(:ResourcesController)
|
|
24
|
+
load "json_api/controllers/resources_controller.rb"
|
|
22
25
|
end
|
|
23
26
|
end
|
|
24
27
|
|
|
@@ -48,6 +51,8 @@ require "json_api/support/responders"
|
|
|
48
51
|
require "json_api/support/instrumentation"
|
|
49
52
|
require "json_api/serialization/serializer"
|
|
50
53
|
require "json_api/serialization/deserializer"
|
|
54
|
+
require "json_api/sideposting/lid_resolver"
|
|
55
|
+
require "json_api/sideposting/processor"
|
|
51
56
|
require "json_api/controllers/concerns/controller_helpers"
|
|
52
57
|
require "json_api/controllers/concerns/resource_actions"
|
|
53
58
|
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: 2.0
|
|
4
|
+
version: 2.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Emil Kampp
|
|
@@ -85,6 +85,13 @@ executables: []
|
|
|
85
85
|
extensions: []
|
|
86
86
|
extra_rdoc_files: []
|
|
87
87
|
files:
|
|
88
|
+
- ".claude/skills/nasa-power-of-ten-ruby/SKILL.md"
|
|
89
|
+
- ".claude/skills/root-cause-analysis/SKILL.md"
|
|
90
|
+
- ".claude/skills/skills-and-subagents/SKILL.md"
|
|
91
|
+
- ".cursor/agents/bias-reviewer.md"
|
|
92
|
+
- ".cursor/agents/lint-format.md"
|
|
93
|
+
- ".cursor/agents/nasa-power-of-ten-reviewer.md"
|
|
94
|
+
- ".cursor/agents/systematic-debugging.md"
|
|
88
95
|
- ".cursor/rules/release.mdc"
|
|
89
96
|
- ".gitignore"
|
|
90
97
|
- ".rspec"
|
|
@@ -127,6 +134,8 @@ files:
|
|
|
127
134
|
- lib/json_api/controllers/concerns/resource_actions/pagination.rb
|
|
128
135
|
- lib/json_api/controllers/concerns/resource_actions/resource_loading.rb
|
|
129
136
|
- lib/json_api/controllers/concerns/resource_actions/serialization.rb
|
|
137
|
+
- lib/json_api/controllers/concerns/resource_actions/sideposting.rb
|
|
138
|
+
- lib/json_api/controllers/concerns/resource_actions/sideposting_primary_first.rb
|
|
130
139
|
- lib/json_api/controllers/concerns/resource_actions/type_validation.rb
|
|
131
140
|
- lib/json_api/controllers/relationships_controller.rb
|
|
132
141
|
- lib/json_api/controllers/resources_controller.rb
|
|
@@ -158,6 +167,9 @@ files:
|
|
|
158
167
|
- lib/json_api/serialization/deserializer.rb
|
|
159
168
|
- lib/json_api/serialization/include_path_helpers.rb
|
|
160
169
|
- lib/json_api/serialization/serializer.rb
|
|
170
|
+
- lib/json_api/sideposting/lid_resolver.rb
|
|
171
|
+
- lib/json_api/sideposting/order.rb
|
|
172
|
+
- lib/json_api/sideposting/processor.rb
|
|
161
173
|
- lib/json_api/support/active_storage_support.rb
|
|
162
174
|
- lib/json_api/support/collection_query.rb
|
|
163
175
|
- lib/json_api/support/concerns/condition_building.rb
|