jpie 3.1.3 → 3.1.6
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 +14 -0
- data/Gemfile.lock +1 -1
- data/README.md +0 -114
- data/lib/json_api/controllers/concerns/controller_helpers/error_rendering.rb +0 -18
- data/lib/json_api/controllers/concerns/resource_actions/crud_helpers.rb +2 -19
- data/lib/json_api/controllers/concerns/resource_actions/resource_loading.rb +4 -3
- data/lib/json_api/controllers/concerns/resource_actions/serialization.rb +0 -24
- data/lib/json_api/controllers/concerns/resource_actions.rb +4 -18
- data/lib/json_api/controllers/relationships_controller.rb +8 -2
- data/lib/json_api/railtie.rb +10 -1
- data/lib/json_api/resources/concerns/attributes_dsl.rb +15 -0
- data/lib/json_api/resources/resource.rb +0 -4
- data/lib/json_api/serialization/concerns/attributes_deserialization.rb +2 -3
- data/lib/json_api/serialization/concerns/deserialization_helpers.rb +3 -11
- data/lib/json_api/serialization/concerns/relationship_processing.rb +2 -2
- data/lib/json_api/serialization/concerns/relationships_deserialization.rb +0 -4
- data/lib/json_api/serialization/deserializer.rb +6 -2
- data/lib/json_api/support/param_helpers.rb +12 -0
- data/lib/json_api/support/relationship_guard.rb +6 -1
- data/lib/json_api/support/relationship_helpers.rb +0 -4
- data/lib/json_api/support/resource_identifier.rb +0 -4
- data/lib/json_api/version.rb +1 -1
- data/lib/json_api.rb +0 -2
- metadata +1 -6
- data/lib/json_api/controllers/concerns/resource_actions/sideposting.rb +0 -97
- data/lib/json_api/controllers/concerns/resource_actions/sideposting_primary_first.rb +0 -63
- data/lib/json_api/sideposting/lid_resolver.rb +0 -72
- data/lib/json_api/sideposting/order.rb +0 -11
- data/lib/json_api/sideposting/processor.rb +0 -118
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ac6d32dde9d9be0163f1f3c48a9c65770f4465176a403efdb5f0ba42f18bb4f5
|
|
4
|
+
data.tar.gz: 4ac43aa7aaf9194497910650cc1180800952806a7cb5ebf1ecf590decda4aaa0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ec7aff7ee080d6acca4bcf19cd3d72f7084c3bcb492df1d6e5c4b674711575450bd03e558267889d46cc9c436255dbc3013fc1b5bfa3df074503fdb39f342737
|
|
7
|
+
data.tar.gz: 6a4c2811e9df0bad8bbecc8b6a62ae5a0f9e44912d0035fad7c22a6569b383e24b574dbb35fe2cc3c9402b8157079af4f0addbef91e685e997539200c9b317b5
|
data/.rubocop.yml
CHANGED
|
@@ -92,6 +92,20 @@ Style/HashSyntax:
|
|
|
92
92
|
Lint/UnusedBlockArgument:
|
|
93
93
|
Exclude:
|
|
94
94
|
- "spec/jsonapi/authorization_spec.rb"
|
|
95
|
+
# Security PoC specs configure authorization handlers via keyword lambdas
|
|
96
|
+
# that intentionally ignore some of the required keyword arguments.
|
|
97
|
+
- "spec/requests/regression/*_spec.rb"
|
|
98
|
+
|
|
99
|
+
# Security PoC regression specs read as a single sequential narrative
|
|
100
|
+
# (sanity check -> attack -> assert), so a handful of expectations per example
|
|
101
|
+
# is clearer than splitting them across examples.
|
|
102
|
+
RSpec/MultipleExpectations:
|
|
103
|
+
Exclude:
|
|
104
|
+
- "spec/requests/regression/*_spec.rb"
|
|
105
|
+
|
|
106
|
+
RSpec/ExampleLength:
|
|
107
|
+
Exclude:
|
|
108
|
+
- "spec/requests/regression/*_spec.rb"
|
|
95
109
|
|
|
96
110
|
# RSpec - file-specific exclusions
|
|
97
111
|
RSpec/DescribedClass:
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
|
@@ -15,7 +15,6 @@ A Rails 8+ gem that provides JSON:API compliant routing DSL and generic JSON:API
|
|
|
15
15
|
- Content negotiation with Accept and Content-Type headers
|
|
16
16
|
- Support for polymorphic and STI relationships
|
|
17
17
|
- Namespace support for API versioning and modular organization
|
|
18
|
-
- Sideposting: create primary and related resources in one request using `included` and local identifiers (`lid`)
|
|
19
18
|
|
|
20
19
|
## Installation
|
|
21
20
|
|
|
@@ -1192,119 +1191,6 @@ The gem validates relationship data:
|
|
|
1192
1191
|
- Invalid polymorphic type (class doesn't exist) returns `400 Bad Request`
|
|
1193
1192
|
- Attempting to unset linkage that cannot be nullified (e.g., foreign key has NOT NULL constraint) returns `400 Bad Request`
|
|
1194
1193
|
|
|
1195
|
-
## Sideposting
|
|
1196
|
-
|
|
1197
|
-
Sideposting lets you create a primary resource and related resources that do not yet exist in a single POST request. The client sends the primary resource in `data` and full resource objects in `included`. Relationships on the primary can reference included resources by **local identifier** (`lid`) instead of `id`. By default the server creates the **primary** first, then creates included resources and updates the primary with resolved relationship ids (**primary-first**). You can optionally use **included-first** (create included resources first, then the primary) via a controller macro.
|
|
1198
|
-
|
|
1199
|
-
This follows the JSON:API 1.1 [Local Identity](https://jsonapi.org/format/1.1/#document-resource-object-identification) convention: `lid` is a client-chosen temporary identifier for the request only; it is not stored.
|
|
1200
|
-
|
|
1201
|
-
### Sidepost order
|
|
1202
|
-
|
|
1203
|
-
Controllers use **primary-first** by default: the primary resource is saved first (without lid-based relationships), then included resources are created, then the primary is updated with the resolved relationship ids. This supports flows such as signup where the user must exist before the account links to them.
|
|
1204
|
-
|
|
1205
|
-
To use the legacy **included-first** order (create included resources first, then the primary), set the macro in your controller:
|
|
1206
|
-
|
|
1207
|
-
```ruby
|
|
1208
|
-
class SomeController < JSONAPI::ResourcesController
|
|
1209
|
-
sidepost_order :included_first
|
|
1210
|
-
end
|
|
1211
|
-
```
|
|
1212
|
-
|
|
1213
|
-
Valid values are `:primary_first` (default) and `:included_first`. Any other value raises `ArgumentError` at class load time.
|
|
1214
|
-
|
|
1215
|
-
### Custom create strategy
|
|
1216
|
-
|
|
1217
|
-
For included resources, the default creation is `model_class.new(params).save`. You can override this per resource type by defining a class method **`sidepost_create`** on the resource class. It receives `params:` (the deserialized model attributes hash) and `controller:` and must return a persisted record or `nil`. If it returns a record, that record is used and the default create path is skipped; if it returns `nil`, the default create runs.
|
|
1218
|
-
|
|
1219
|
-
Example: use a service to create the related resource instead of a bare `new`/`save`:
|
|
1220
|
-
|
|
1221
|
-
```ruby
|
|
1222
|
-
class AccountResource < JSONAPI::Resource
|
|
1223
|
-
def self.sidepost_create(params:, controller:)
|
|
1224
|
-
CreateAccountService.call(
|
|
1225
|
-
name: params["name"],
|
|
1226
|
-
domain: params["domain"],
|
|
1227
|
-
creator: Current.user
|
|
1228
|
-
).account
|
|
1229
|
-
end
|
|
1230
|
-
end
|
|
1231
|
-
```
|
|
1232
|
-
|
|
1233
|
-
Use **primary-first** (default) plus `sidepost_create` on the related resource when the primary must exist first and the related resource needs custom creation logic.
|
|
1234
|
-
|
|
1235
|
-
### Request format
|
|
1236
|
-
|
|
1237
|
-
- **`data`**: The primary resource. In `relationships`, use `type` and `lid` (no `id`) to reference an item in `included`.
|
|
1238
|
-
- **`included`**: Array of full resource objects. Each can have `type`, `lid`, and `attributes` (and optionally `relationships` with further `lid` references). Order of creation is the order of `included`; a later item may reference an earlier item’s `lid`.
|
|
1239
|
-
|
|
1240
|
-
Example: create a user and a new profile in one request:
|
|
1241
|
-
|
|
1242
|
-
```http
|
|
1243
|
-
POST /users HTTP/1.1
|
|
1244
|
-
Content-Type: application/vnd.api+json
|
|
1245
|
-
Accept: application/vnd.api+json
|
|
1246
|
-
|
|
1247
|
-
{
|
|
1248
|
-
"data": {
|
|
1249
|
-
"type": "users",
|
|
1250
|
-
"attributes": {
|
|
1251
|
-
"name": "Jane Doe",
|
|
1252
|
-
"email": "jane@example.com"
|
|
1253
|
-
},
|
|
1254
|
-
"relationships": {
|
|
1255
|
-
"profile": {
|
|
1256
|
-
"data": { "type": "customer_profiles", "lid": "temp-profile" }
|
|
1257
|
-
}
|
|
1258
|
-
}
|
|
1259
|
-
},
|
|
1260
|
-
"included": [
|
|
1261
|
-
{
|
|
1262
|
-
"type": "customer_profiles",
|
|
1263
|
-
"lid": "temp-profile",
|
|
1264
|
-
"attributes": {
|
|
1265
|
-
"company_name": "Acme Inc",
|
|
1266
|
-
"industry": "Tech"
|
|
1267
|
-
}
|
|
1268
|
-
}
|
|
1269
|
-
]
|
|
1270
|
-
}
|
|
1271
|
-
```
|
|
1272
|
-
|
|
1273
|
-
With the default **primary-first** order, the server creates the user first, then creates the `customer_profiles` resource and assigns it an `id`, then updates the user's `profile` relationship to point to that new profile. The `lid` `"temp-profile"` is only used to match the relationship to the included object.
|
|
1274
|
-
|
|
1275
|
-
### Response
|
|
1276
|
-
|
|
1277
|
-
On success the response is `201 Created` with the primary resource in `data` and the created related resources in `included`, each with a real `id`:
|
|
1278
|
-
|
|
1279
|
-
```json
|
|
1280
|
-
{
|
|
1281
|
-
"jsonapi": { "version": "1.1" },
|
|
1282
|
-
"data": {
|
|
1283
|
-
"type": "users",
|
|
1284
|
-
"id": "123",
|
|
1285
|
-
"attributes": { "name": "Jane Doe", "email": "jane@example.com" },
|
|
1286
|
-
"relationships": {
|
|
1287
|
-
"profile": { "data": { "type": "customer_profiles", "id": "456" } }
|
|
1288
|
-
}
|
|
1289
|
-
},
|
|
1290
|
-
"included": [
|
|
1291
|
-
{
|
|
1292
|
-
"type": "customer_profiles",
|
|
1293
|
-
"id": "456",
|
|
1294
|
-
"attributes": { "company_name": "Acme Inc", "industry": "Tech" }
|
|
1295
|
-
}
|
|
1296
|
-
]
|
|
1297
|
-
}
|
|
1298
|
-
```
|
|
1299
|
-
|
|
1300
|
-
### Validation and rollback
|
|
1301
|
-
|
|
1302
|
-
Creation runs in a single transaction. If any included resource fails validation, the entire request is rolled back: no primary or included resources are created. The response is `422 Unprocessable Entity` with errors that point to the failing included resource (e.g. `source.pointer` to the relevant `included` index or attribute).
|
|
1303
|
-
|
|
1304
|
-
### When sideposting is used
|
|
1305
|
-
|
|
1306
|
-
Sideposting is applied when the create request includes a top-level `included` array. If `included` is present, the controller uses the configured **sidepost order** (primary-first by default, or included-first if `sidepost_order :included_first` is set). With primary-first, it creates the primary resource first (without lid-based relationships), creates included resources, resolves `lid` references to new `id`s, updates the primary with those relationships, then returns the primary and `included` in the response. With included-first, it creates included resources first, resolves `lid`s, then creates the primary and returns it with `included`.
|
|
1307
|
-
|
|
1308
1194
|
## Relationship Endpoint Details
|
|
1309
1195
|
|
|
1310
1196
|
The gem provides dedicated endpoints for managing relationships independently of the main resource:
|
|
@@ -43,24 +43,6 @@ module JSONAPI
|
|
|
43
43
|
render json: { errors: }, status: :unprocessable_content
|
|
44
44
|
end
|
|
45
45
|
|
|
46
|
-
def render_sidepost_validation_errors(sidepost_error)
|
|
47
|
-
errors = sidepost_validation_errors(sidepost_error)
|
|
48
|
-
render json: { errors: }, status: :unprocessable_content
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
def sidepost_validation_errors(sidepost_error)
|
|
52
|
-
index = sidepost_error.included_index
|
|
53
|
-
sidepost_error.record.errors.map do |error|
|
|
54
|
-
pointer = "/included/#{index}/attributes/#{error.attribute}"
|
|
55
|
-
{
|
|
56
|
-
status: "422",
|
|
57
|
-
title: "Validation Error",
|
|
58
|
-
detail: error.full_message,
|
|
59
|
-
source: { pointer: pointer },
|
|
60
|
-
}
|
|
61
|
-
end
|
|
62
|
-
end
|
|
63
|
-
|
|
64
46
|
def render_parameter_not_allowed_error(error)
|
|
65
47
|
error_params = error.respond_to?(:params) ? error.params : []
|
|
66
48
|
detail = error_params.present? ? error_params.join(", ") : "Relationship or attribute is read-only"
|
|
@@ -8,15 +8,7 @@ module JSONAPI
|
|
|
8
8
|
private
|
|
9
9
|
|
|
10
10
|
def prepare_create_params(sti_class)
|
|
11
|
-
hash = deserialize_params(:create, model_class: sti_class)
|
|
12
|
-
attachments = extract_active_storage_params_from_hash(hash, sti_class)
|
|
13
|
-
clean_params(hash, attachments)
|
|
14
|
-
apply_sti_type(sti_class, hash)
|
|
15
|
-
[hash, attachments]
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
def prepare_create_params_from_data(sti_class, data)
|
|
19
|
-
hash = deserialize_params(:create, model_class: sti_class, data:)
|
|
11
|
+
hash = deserialize_params(:create, model_class: sti_class, definition: determine_sti_resource_class)
|
|
20
12
|
attachments = extract_active_storage_params_from_hash(hash, sti_class)
|
|
21
13
|
clean_params(hash, attachments)
|
|
22
14
|
apply_sti_type(sti_class, hash)
|
|
@@ -24,7 +16,7 @@ module JSONAPI
|
|
|
24
16
|
end
|
|
25
17
|
|
|
26
18
|
def prepare_update_params
|
|
27
|
-
hash = deserialize_params(:update)
|
|
19
|
+
hash = deserialize_params(:update, definition: @resource_class)
|
|
28
20
|
attachments = extract_active_storage_params_from_hash(hash, model_class)
|
|
29
21
|
clean_params(hash, attachments)
|
|
30
22
|
[hash, attachments]
|
|
@@ -53,15 +45,6 @@ module JSONAPI
|
|
|
53
45
|
render json: serialize_resource(resource), status: :created, location: resource_url(resource)
|
|
54
46
|
end
|
|
55
47
|
|
|
56
|
-
def save_created_with_sidepost(resource, sideposted_records)
|
|
57
|
-
raise JSONAPI::Sideposting::PrimaryValidationError, resource unless resource.save
|
|
58
|
-
|
|
59
|
-
emit_resource_event(:created, resource)
|
|
60
|
-
render json: serialize_resource_with_sidepost(resource, sideposted_records),
|
|
61
|
-
status: :created,
|
|
62
|
-
location: resource_url(resource)
|
|
63
|
-
end
|
|
64
|
-
|
|
65
48
|
def save_updated(params_hash, attachments)
|
|
66
49
|
return render_validation_errors(@resource) unless @resource.update(params_hash)
|
|
67
50
|
|
|
@@ -28,7 +28,8 @@ module JSONAPI
|
|
|
28
28
|
def load_resource_record
|
|
29
29
|
return unless params[:id] && @resource_class
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
authorized = apply_authorization_scope(@resource_class.records, action: action_name.to_sym)
|
|
32
|
+
scope = scope_with_includes(authorized)
|
|
32
33
|
@resource = scope.find(params[:id])
|
|
33
34
|
end
|
|
34
35
|
|
|
@@ -51,10 +52,10 @@ module JSONAPI
|
|
|
51
52
|
JSONAPI::ParamHelpers.deep_symbolize_params(data)
|
|
52
53
|
end
|
|
53
54
|
|
|
54
|
-
def deserialize_params(action = :update, model_class: nil, data: nil)
|
|
55
|
+
def deserialize_params(action = :update, model_class: nil, data: nil, definition: nil)
|
|
55
56
|
target = model_class || self.model_class
|
|
56
57
|
payload = data.nil? ? raw_jsonapi_data : symbolize_data(data)
|
|
57
|
-
JSONAPI::Deserializer.new(payload, model_class: target, action:).to_model_attributes
|
|
58
|
+
JSONAPI::Deserializer.new(payload, model_class: target, action:, definition:).to_model_attributes
|
|
58
59
|
end
|
|
59
60
|
|
|
60
61
|
def resource_url(resource)
|
|
@@ -14,19 +14,6 @@ module JSONAPI
|
|
|
14
14
|
)
|
|
15
15
|
end
|
|
16
16
|
|
|
17
|
-
def serialize_resource_with_sidepost(resource, sideposted_records)
|
|
18
|
-
result = serialize_resource(resource)
|
|
19
|
-
return result if sideposted_records.empty?
|
|
20
|
-
|
|
21
|
-
fields = parse_fields_param
|
|
22
|
-
sideposted_objects = sideposted_records.map do |rec|
|
|
23
|
-
JSONAPI::Serializer.new(rec).to_hash(include: [], fields:, document_meta: nil)[:data]
|
|
24
|
-
end
|
|
25
|
-
all_included = (result[:included] || []) + sideposted_objects
|
|
26
|
-
result[:included] = deduplicate_included(all_included)
|
|
27
|
-
result
|
|
28
|
-
end
|
|
29
|
-
|
|
30
17
|
def serialize_collection(resources)
|
|
31
18
|
includes = parse_include_param
|
|
32
19
|
fields = parse_fields_param
|
|
@@ -72,17 +59,6 @@ module JSONAPI
|
|
|
72
59
|
processed.add(key)
|
|
73
60
|
end
|
|
74
61
|
|
|
75
|
-
def deduplicate_included(included_array)
|
|
76
|
-
processed = Set.new
|
|
77
|
-
included_array.each_with_object([]) do |inc, out|
|
|
78
|
-
key = "#{inc[:type]}-#{inc[:id]}"
|
|
79
|
-
next if processed.include?(key)
|
|
80
|
-
|
|
81
|
-
out << inc
|
|
82
|
-
processed.add(key)
|
|
83
|
-
end
|
|
84
|
-
end
|
|
85
|
-
|
|
86
62
|
def build_collection_response(data, all_included)
|
|
87
63
|
result = { jsonapi: JSONAPI::Serializer.jsonapi_object, data: }
|
|
88
64
|
result[:included] = all_included if all_included.any?
|
|
@@ -9,7 +9,6 @@ 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"
|
|
13
12
|
|
|
14
13
|
module JSONAPI
|
|
15
14
|
module ResourceActions
|
|
@@ -23,7 +22,6 @@ module JSONAPI
|
|
|
23
22
|
include TypeValidation
|
|
24
23
|
include CrudHelpers
|
|
25
24
|
include ResourceLoading
|
|
26
|
-
include Sideposting
|
|
27
25
|
|
|
28
26
|
included do
|
|
29
27
|
before_action :load_jsonapi_resource
|
|
@@ -49,28 +47,16 @@ module JSONAPI
|
|
|
49
47
|
end
|
|
50
48
|
|
|
51
49
|
def create
|
|
52
|
-
|
|
50
|
+
@resource = build_resource_for_create
|
|
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)
|
|
53
54
|
rescue ArgumentError => e
|
|
54
55
|
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)
|
|
59
56
|
rescue JSONAPI::Errors::ParameterNotAllowed, ActiveSupport::MessageVerifier::InvalidSignature => e
|
|
60
57
|
handle_create_exception(e)
|
|
61
58
|
end
|
|
62
59
|
|
|
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
|
-
|
|
74
60
|
def build_resource_for_create
|
|
75
61
|
sti_class = determine_sti_class
|
|
76
62
|
params_hash, @create_attachments = prepare_create_params(sti_class)
|
|
@@ -78,9 +78,11 @@ module JSONAPI
|
|
|
78
78
|
raw_data = params[:data]
|
|
79
79
|
return nil if raw_data.nil?
|
|
80
80
|
return [] if raw_data.is_a?(Array) && raw_data.empty?
|
|
81
|
-
|
|
81
|
+
if raw_data.is_a?(Array)
|
|
82
|
+
return raw_data.map { |item| ParamHelpers.deep_symbolize_params(ParamHelpers.ensure_object!(item, "data")) }
|
|
83
|
+
end
|
|
82
84
|
|
|
83
|
-
ParamHelpers.deep_symbolize_params(raw_data)
|
|
85
|
+
ParamHelpers.deep_symbolize_params(ParamHelpers.ensure_object!(raw_data, "data"))
|
|
84
86
|
end
|
|
85
87
|
|
|
86
88
|
def polymorphic_association?
|
|
@@ -90,6 +92,10 @@ module JSONAPI
|
|
|
90
92
|
def ensure_relationship_writable!(association)
|
|
91
93
|
return if active_storage_writable?(association)
|
|
92
94
|
|
|
95
|
+
unless @resource_class.relationship_field_writable?(@relationship_name, :update)
|
|
96
|
+
raise JSONAPI::AuthorizationError, "Relationship '#{@relationship_name}' is not updatable"
|
|
97
|
+
end
|
|
98
|
+
|
|
93
99
|
relationship_def = find_relationship_definition
|
|
94
100
|
readonly = relationship_def && (relationship_def[:options] || {})[:readonly] == true
|
|
95
101
|
|
data/lib/json_api/railtie.rb
CHANGED
|
@@ -57,7 +57,16 @@ module JSONAPI
|
|
|
57
57
|
|
|
58
58
|
initializer "json_api.parameter_parsing", after: "action_dispatch.configure" do |_app|
|
|
59
59
|
ActionDispatch::Request.parameter_parsers[:jsonapi] = lambda do |raw_post|
|
|
60
|
-
ActiveSupport::JSON.decode(raw_post)
|
|
60
|
+
data = ActiveSupport::JSON.decode(raw_post)
|
|
61
|
+
# A JSON:API document must be a single object. A top-level non-object body (a bare
|
|
62
|
+
# array/scalar) would otherwise leave `request.parameters` a non-Hash, and Rails' own
|
|
63
|
+
# Parameters#parameters then raises NoMethodError (`merge`/`deep_transform_keys!`) -> an
|
|
64
|
+
# unhandled 500. Reject it as a parse error so Rails renders a clean 400.
|
|
65
|
+
unless data.is_a?(Hash)
|
|
66
|
+
raise ActionDispatch::Http::Parameters::ParseError, "JSON:API request body must be an object"
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
data
|
|
61
70
|
rescue JSON::ParserError => e
|
|
62
71
|
raise ActionDispatch::Http::Parameters::ParseError, e.message
|
|
63
72
|
end
|
|
@@ -41,6 +41,21 @@ module JSONAPI
|
|
|
41
41
|
resolve_field_list(:@updatable_fields, :permitted_updatable_fields)
|
|
42
42
|
end
|
|
43
43
|
|
|
44
|
+
# Whether a relationship may be written for the given action (:create or :update).
|
|
45
|
+
# A relationship is gated by the creatable_fields/updatable_fields allow-list only when
|
|
46
|
+
# the resource opted it in by naming it in one of those lists; relationships named in
|
|
47
|
+
# neither keep the default (readonly-only) behavior. This lets a resource make a
|
|
48
|
+
# relationship create-only — name it in creatable_fields, omit it from updatable_fields —
|
|
49
|
+
# the same way it restricts attributes.
|
|
50
|
+
def relationship_field_writable?(name, action)
|
|
51
|
+
name = name.to_sym
|
|
52
|
+
creatable = permitted_creatable_fields.map(&:to_sym)
|
|
53
|
+
updatable = permitted_updatable_fields.map(&:to_sym)
|
|
54
|
+
return true unless creatable.include?(name) || updatable.include?(name)
|
|
55
|
+
|
|
56
|
+
(action == :create ? creatable : updatable).include?(name)
|
|
57
|
+
end
|
|
58
|
+
|
|
44
59
|
def resolve_field_list(ivar, method)
|
|
45
60
|
return (instance_variable_get(ivar) || []).uniq if instance_variable_defined?(ivar)
|
|
46
61
|
return superclass.public_send(method).uniq if inherits_field?(ivar, method)
|
|
@@ -4,10 +4,9 @@ module JSONAPI
|
|
|
4
4
|
module Serialization
|
|
5
5
|
module AttributesDeserialization
|
|
6
6
|
def attributes
|
|
7
|
-
attrs = extract_attributes_from_params
|
|
8
|
-
attrs = attrs.transform_keys(&:to_sym) if attrs.respond_to?(:transform_keys)
|
|
7
|
+
attrs = ParamHelpers.ensure_object!(extract_attributes_from_params, "data.attributes")
|
|
9
8
|
permitted_attrs = permitted_attributes_for_action
|
|
10
|
-
attrs.slice(*permitted_attrs)
|
|
9
|
+
attrs.transform_keys(&:to_sym).slice(*permitted_attrs)
|
|
11
10
|
end
|
|
12
11
|
|
|
13
12
|
def extract_attributes_from_params
|
|
@@ -52,11 +52,6 @@ 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
|
|
60
55
|
raise ArgumentError, "Missing id in relationship data" unless id
|
|
61
56
|
|
|
62
57
|
id
|
|
@@ -99,17 +94,13 @@ module JSONAPI
|
|
|
99
94
|
def valid_resource_identifier?(identifier)
|
|
100
95
|
return false unless identifier.is_a?(Hash)
|
|
101
96
|
|
|
102
|
-
has_type?(identifier) &&
|
|
97
|
+
has_type?(identifier) && has_id?(identifier)
|
|
103
98
|
end
|
|
104
99
|
|
|
105
100
|
def has_id?(identifier)
|
|
106
101
|
identifier[:id].present?
|
|
107
102
|
end
|
|
108
103
|
|
|
109
|
-
def lid?(identifier)
|
|
110
|
-
identifier[:lid].to_s.present?
|
|
111
|
-
end
|
|
112
|
-
|
|
113
104
|
def has_type?(identifier)
|
|
114
105
|
identifier[:type]
|
|
115
106
|
end
|
|
@@ -129,7 +120,8 @@ module JSONAPI
|
|
|
129
120
|
|
|
130
121
|
association = @model_class.reflect_on_association(association_name.to_sym)
|
|
131
122
|
readonly = relationship_options_for(association_name)[:readonly] == true
|
|
132
|
-
|
|
123
|
+
field_writable = @definition.relationship_field_writable?(association_name, @action)
|
|
124
|
+
JSONAPI::RelationshipGuard.ensure_writable!(association, association_name, readonly:, field_writable:)
|
|
133
125
|
end
|
|
134
126
|
|
|
135
127
|
def relationship_options_for(association_name)
|
|
@@ -21,7 +21,7 @@ module JSONAPI
|
|
|
21
21
|
private
|
|
22
22
|
|
|
23
23
|
def normalize_relationship_value(value)
|
|
24
|
-
|
|
24
|
+
ParamHelpers.ensure_object!(value, "relationship")
|
|
25
25
|
end
|
|
26
26
|
|
|
27
27
|
def extract_data_from_value(value_hash)
|
|
@@ -48,7 +48,7 @@ module JSONAPI
|
|
|
48
48
|
def validate_relationship_data_format!(data, association_name)
|
|
49
49
|
return if valid_relationship_data?(data)
|
|
50
50
|
|
|
51
|
-
raise ArgumentError, "Invalid relationship data for #{association_name}: missing type or
|
|
51
|
+
raise ArgumentError, "Invalid relationship data for #{association_name}: missing type or id"
|
|
52
52
|
end
|
|
53
53
|
|
|
54
54
|
def process_relationship_data(attrs, association_name, param_name, data)
|
|
@@ -47,10 +47,6 @@ module JSONAPI
|
|
|
47
47
|
RelationshipHelpers.extract_id_from_identifier(identifier)
|
|
48
48
|
end
|
|
49
49
|
|
|
50
|
-
def extract_lid_from_identifier(identifier)
|
|
51
|
-
RelationshipHelpers.extract_lid_from_identifier(identifier)
|
|
52
|
-
end
|
|
53
|
-
|
|
54
50
|
def relationship_id(relationship_name)
|
|
55
51
|
relationship_ids(relationship_name).first
|
|
56
52
|
end
|
|
@@ -16,10 +16,14 @@ module JSONAPI
|
|
|
16
16
|
include Serialization::RelationshipProcessing
|
|
17
17
|
include Serialization::DeserializationHelpers
|
|
18
18
|
|
|
19
|
-
def initialize(params, model_class:, action: :create)
|
|
19
|
+
def initialize(params, model_class:, action: :create, definition: nil)
|
|
20
20
|
@params = ParamHelpers.deep_symbolize_params(params)
|
|
21
21
|
@model_class = model_class
|
|
22
|
-
|
|
22
|
+
# Prefer the resource the request is addressing (resolved by type/route). Resolving
|
|
23
|
+
# from the model alone is ambiguous when several resources share one model
|
|
24
|
+
# (e.g. RestrictedPostResource and PostResource both back Post), which would gate
|
|
25
|
+
# relationships against the wrong resource's creatable_fields/updatable_fields.
|
|
26
|
+
@definition = definition || ResourceLoader.find_for_model(model_class)
|
|
23
27
|
@action = action.to_sym
|
|
24
28
|
end
|
|
25
29
|
end
|
|
@@ -30,6 +30,18 @@ module JSONAPI
|
|
|
30
30
|
end
|
|
31
31
|
end
|
|
32
32
|
|
|
33
|
+
# Every JSON:API document member that carries attributes or resource identifiers must be an
|
|
34
|
+
# object. A malformed document can send a scalar/array in that slot; without this guard a
|
|
35
|
+
# later `.to_h`/`.slice`/`.dig` raises NoMethodError/TypeError -> an unhandled 500. Coerce
|
|
36
|
+
# a Parameters to a Hash, pass a Hash through, and reject anything else with an ArgumentError
|
|
37
|
+
# (which the create/update/relationship rescues render as a clean 400).
|
|
38
|
+
def ensure_object!(value, member)
|
|
39
|
+
return value if value.is_a?(Hash)
|
|
40
|
+
return value.to_unsafe_h if value.respond_to?(:to_unsafe_h)
|
|
41
|
+
|
|
42
|
+
raise ArgumentError, "'#{member}' must be an object"
|
|
43
|
+
end
|
|
44
|
+
|
|
33
45
|
def build_query_string(query_params)
|
|
34
46
|
query_parts = []
|
|
35
47
|
query_params.each do |key, value|
|
|
@@ -6,7 +6,12 @@ module JSONAPI
|
|
|
6
6
|
module RelationshipGuard
|
|
7
7
|
module_function
|
|
8
8
|
|
|
9
|
-
def ensure_writable!(association, error_target, readonly: false)
|
|
9
|
+
def ensure_writable!(association, error_target, readonly: false, field_writable: true)
|
|
10
|
+
# A relationship excluded from the resource's creatable_fields/updatable_fields
|
|
11
|
+
# allow-list is not writable for this action, on either the main-body path or the
|
|
12
|
+
# dedicated endpoint. This gate is independent of the AR reflection.
|
|
13
|
+
raise JSONAPI::AuthorizationError, "Relationship '#{error_target}' is not writable" unless field_writable
|
|
14
|
+
|
|
10
15
|
return unless association
|
|
11
16
|
return unless readonly
|
|
12
17
|
|
|
@@ -35,10 +35,6 @@ module JSONAPI
|
|
|
35
35
|
ResourceIdentifier.extract_id(identifier)
|
|
36
36
|
end
|
|
37
37
|
|
|
38
|
-
def extract_lid_from_identifier(identifier)
|
|
39
|
-
ResourceIdentifier.extract_lid(identifier)
|
|
40
|
-
end
|
|
41
|
-
|
|
42
38
|
def extract_type_from_identifier(identifier)
|
|
43
39
|
ResourceIdentifier.extract_type(identifier)
|
|
44
40
|
end
|
data/lib/json_api/version.rb
CHANGED
data/lib/json_api.rb
CHANGED
|
@@ -50,8 +50,6 @@ require "json_api/support/responders"
|
|
|
50
50
|
require "json_api/support/instrumentation"
|
|
51
51
|
require "json_api/serialization/serializer"
|
|
52
52
|
require "json_api/serialization/deserializer"
|
|
53
|
-
require "json_api/sideposting/lid_resolver"
|
|
54
|
-
require "json_api/sideposting/processor"
|
|
55
53
|
require "json_api/controllers/concerns/controller_helpers"
|
|
56
54
|
require "json_api/controllers/concerns/resource_actions"
|
|
57
55
|
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: 3.1.
|
|
4
|
+
version: 3.1.6
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Emil Kampp
|
|
@@ -135,8 +135,6 @@ files:
|
|
|
135
135
|
- lib/json_api/controllers/concerns/resource_actions/pagination.rb
|
|
136
136
|
- lib/json_api/controllers/concerns/resource_actions/resource_loading.rb
|
|
137
137
|
- lib/json_api/controllers/concerns/resource_actions/serialization.rb
|
|
138
|
-
- lib/json_api/controllers/concerns/resource_actions/sideposting.rb
|
|
139
|
-
- lib/json_api/controllers/concerns/resource_actions/sideposting_primary_first.rb
|
|
140
138
|
- lib/json_api/controllers/concerns/resource_actions/type_validation.rb
|
|
141
139
|
- lib/json_api/controllers/relationships_controller.rb
|
|
142
140
|
- lib/json_api/controllers/resources_controller.rb
|
|
@@ -168,9 +166,6 @@ files:
|
|
|
168
166
|
- lib/json_api/serialization/deserializer.rb
|
|
169
167
|
- lib/json_api/serialization/include_path_helpers.rb
|
|
170
168
|
- lib/json_api/serialization/serializer.rb
|
|
171
|
-
- lib/json_api/sideposting/lid_resolver.rb
|
|
172
|
-
- lib/json_api/sideposting/order.rb
|
|
173
|
-
- lib/json_api/sideposting/processor.rb
|
|
174
169
|
- lib/json_api/support/active_storage_support.rb
|
|
175
170
|
- lib/json_api/support/collection_query.rb
|
|
176
171
|
- lib/json_api/support/concerns/condition_building.rb
|
|
@@ -1,97 +0,0 @@
|
|
|
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
|
|
@@ -1,63 +0,0 @@
|
|
|
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
|
|
@@ -1,72 +0,0 @@
|
|
|
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
|
|
@@ -1,118 +0,0 @@
|
|
|
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
|