- 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
- This approach is not RESTful
Add this line to your application's Gemfile:
gem 'deep_unrest'And then execute:
$ bundle-
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
For the following examples, consider this data model:
Update attributes on a single Submission with id 123
// PATCH /deep_unrest/update
{
redirect: '/api/submissions/123',
data: [
{
path: 'submissions.123',
attributes: {
approved: true
}
}
]
}The success action is to follow the redirect request param
(/api/submissions/123 in the example above).
{
id: 123,
type: 'submissions',
attributes: {
approved: 'true'
}
}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'"
}
]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'"
}
]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',
}
]To delete a resource, pass the param destroy: true along with the path to that resource.
// PATCH /deep_unrest/update
{
data: [
{
path: 'submissions.123',
destroy: true,
}
]
}When no redirect path is specified, an empty object will be returned as the response.
{}When creating new resources, the client should assign a temporary ID to the new
resource. The temporary ID should be surrounded in brackets ([]).
// PATCH /deep_unrest/update
{
redirect: '/submissions/[1]',
data: [
{
path: 'submissions[1]',
attributes: {
name: 'testing'
}
}
]
}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'
}
}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',
}
]This shows an example of a complex operation involving multiple resources. This example will perform the following operations:
- Change the
namecolumn ofSubmissionwith id123totest - Change the
valuecolumn ofAnswerwith id1toyes - Create a new
Answerwith a value ofnousing temp ID[1] - Delete the
Answerwith id2
These operations will be performed within a single ActiveRecord transaction.
// 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
}
]
}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.
// PATCH /deep_unrest/update
{
redirect: '/api/submissions',
data: [
{
path: 'submissions.*',
attributes: {
approved: true
}
}
]
}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.
// PATCH /deep_unrest/update
{
redirect: '/api/submissions',
data: [
{
path: 'submissions.*',
destroy: true
}
]
}- Allow the use of filters when performing bulk operations.
- How should we handle nested bulk operations? i.e.
submissions.*.questions.*.answers.*
TDB
The gem is available as open source under the terms of the WTFPL.

