0.01
A long-lived project that still receives updates
Update multiple or deeply nested JSONAPI resources
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
 Dependencies

Development

= 1.3.0
= 2.18.0
= 2.1.0

Runtime

~> 6.1.3
 Project Readme

CircleCI Test Coverage Code Climate Issue Count

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

  1. Mount the endpoint:

    # config/routes.rb
    Rails.application.routes.draw do
      # ...
      mount DeepUnrest::Engine => '/deep_unrest'
    end
  2. 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 of Submission with id 123 to test
  • Change the value column of Answer with id 1 to yes
  • Create a new Answer with a value of no using temp ID [1]
  • Delete the Answer with id 2

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.