Goals
- Allow updates and deletions of deeply nested resources
- Allow bulk updates and deletions
- Perform authorization on all affected resources
- Control which params each user is allowed to update for each resource
- Provide clear errors to the client
Caveats
- This approach is not RESTful
Installation
Add this line to your application's Gemfile:
gem 'deep_unrest'
And then execute:
$ bundle
Configuration
-
Mount the endpoint:
# config/routes.rb Rails.application.routes.draw do # ... mount DeepUnrest::Engine => '/deep_unrest' end
-
Set the authentication concern and authorization strategy:
# config/initailizers/deep_unrest.rb DeepUnrest.configure do |config| # will be included by the controller as a concern config.authentication_concern = DeviseTokenAuth::Concerns::SetUserByToken # will be called from the controller to identify the current user config.get_user = proc { current_user } # or if your app has multiple user types: # config.get_user = proc { current_admin || current_user } # stategy that will be used to authorize the current user for each resource config.authorization_strategy = DeepUnrest::Authorization::PunditStrategy end
Usage
For the following examples, consider this data model:
Example 1 - Simple Update:
Update attributes on a single Submission
with id 123
Request:
// PATCH /deep_unrest/update
{
redirect: '/api/submissions/123',
data: [
{
path: 'submissions.123',
attributes: {
approved: true
}
}
]
}
200 Response:
The success action is to follow the redirect
request param
(/api/submissions/123
in the example above).
{
id: 123,
type: 'submissions',
attributes: {
approved: 'true'
}
}
403 Response:
This error will occur when a user attempts to update a resource that is not within their policy scope.
[
{
source: { pointer: { 'submissions.123' } },
title: "User with id '456' is not authorized to update Submission with id '123'"
}
]
405 Response:
This error will occur when a user is allowed to update a resource, but not specified attributes of that resource.
[
{
source: { pointer: { 'submissions.123' } },
title: "Attributes [:approved] of Submission not allowed to Applicant with id '789'"
}
]
409 Response:
This error will occur when field-level validation fails on any resource updates.
[
{
source: { pointer: { 'submissions.123.name' } },
title: 'Name is required',
detail: 'is required',
}
]
Example 2 - Simple Delete:
To delete a resource, pass the param destroy: true
along with the path to that resource.
Request:
// PATCH /deep_unrest/update
{
data: [
{
path: 'submissions.123',
destroy: true,
}
]
}
200 Response:
When no redirect path is specified, an empty object will be returned as the response.
{}
Example 3 - Simple Create:
When creating new resources, the client should assign a temporary ID to the new
resource. The temporary ID should be surrounded in brackets ([]
).
Create Request
// PATCH /deep_unrest/update
{
redirect: '/submissions/[1]',
data: [
{
path: 'submissions[1]',
attributes: {
name: 'testing'
}
}
]
}
200 Response:
When a temp id ([id]
) is present in the redirect url, the temp id will be
replaced with the id given to the new resource.
Using the example above, assuming that the Submission
at path
submissions[1]
was given the id 123
, the redirect request param of
/submissions/[1]
will be replaced with /submissions/123
.
{
id: 123,
type: 'submissions',
attributes: {
name: 'testing'
}
}
Create Errors:
All errors regarding the new resource will use the temp ID as the path to the error.
[
{
source: { pointer: { 'submissions[123].name' } },
title: 'Name is invalid',
detail: 'is invalid',
}
]
Example 4 - Complex Nested Update:
This shows an example of a complex operation involving multiple resources. This example will perform the following operations:
- Change the
name
column ofSubmission
with id123
totest
- Change the
value
column ofAnswer
with id1
toyes
- Create a new
Answer
with a value ofno
using temp ID[1]
- Delete the
Answer
with id2
These operations will be performed within a single ActiveRecord
transaction.
Complex Nested Update Request
// PATCH /deep_unrest/update
{
redirect: '/api/submissions/123',
data: [
{
path: 'submissions.123',
attributes: { name: 'test' }
},
{
path: "submissions.123.questions.456.answers.1",
attributes: { value: 'yes' }
},
{
path: "submissions.123.questions.456.answers[1]",
attributes: {
value: 'no',
questionId: 456,
submissionId: 123,
applicantId: 890
}
},
{
path: "submissions.123.questions.456.answers.2",
destroy: true
}
]
}
Example 5 - Bulk Updates
The following example will mark every Submission
as approved
.
When using an authorization strategy, the scope of the bulk update will be limited to the current user's allowed scope.
Bulk Update Request
// PATCH /deep_unrest/update
{
redirect: '/api/submissions',
data: [
{
path: 'submissions.*',
attributes: {
approved: true
}
}
]
}
Example 6 - Bulk Delete
The following example will delete every Submission
.
When using an authorization strategy, the scope of the bulk delete will be limited to the current user's allowed scope.
Bulk Delete Request
// PATCH /deep_unrest/update
{
redirect: '/api/submissions',
data: [
{
path: 'submissions.*',
destroy: true
}
]
}
TODO
- Allow the use of filters when performing bulk operations.
- How should we handle nested bulk operations? i.e.
submissions.*.questions.*.answers.*
Contributing
TDB
License
The gem is available as open source under the terms of the WTFPL.