Skip to content
This repository was archived by the owner on Apr 20, 2019. It is now read-only.
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .ruby-version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
2.3.0
2.3
6 changes: 3 additions & 3 deletions gemfiles/rails_4.gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@
source "https://rubygems.org"

gem "rails", "~> 4.1"
gem "jsonapi-rails", "~> 0.3.1", :require => "jsonapi/rails"
gem "jsonapi-rails", "~> 0.3.1", require: "jsonapi/rails"
gem "rspec-rails"

group :test do
gem "pry"
gem "pry-byebug", :platform => [:mri]
gem "pry-byebug", platform: [:mri]
gem "appraisal"
gem "guard"
gem "guard-rspec"
end

gemspec :path => "../"
gemspec path: "../"
6 changes: 3 additions & 3 deletions gemfiles/rails_5.gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@
source "https://rubygems.org"

gem "rails", "~> 5.2"
gem "jsonapi-rails", "~> 0.3.1", :require => "jsonapi/rails"
gem "jsonapi-rails", "~> 0.3.1", require: "jsonapi/rails"
gem "rspec-rails"

group :test do
gem "pry"
gem "pry-byebug", :platform => [:mri]
gem "pry-byebug", platform: [:mri]
gem "appraisal"
gem "guard"
gem "guard-rspec"
end

gemspec :path => "../"
gemspec path: "../"
11 changes: 10 additions & 1 deletion lib/graphiti/resource.rb
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,16 @@ def resolve(scope)

def before_commit(model, method)
hook = self.class.config[:before_commit][method]
hook.call(model) if hook
if hook
hook.call(model)
else
model
end
end

def on_rollback(record, method)
hook = self.class.config[:on_rollback][method]
hook.call(record) if hook
end

def transaction
Expand Down
1 change: 1 addition & 0 deletions lib/graphiti/resource/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@ def config
sorts: {},
pagination: nil,
before_commit: {},
on_rollback: {},
attributes: {},
extra_attributes: {},
sideloads: {}
Expand Down
6 changes: 6 additions & 0 deletions lib/graphiti/resource/dsl.rb
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,12 @@ def before_commit(only: [:create, :update, :destroy], &blk)
end
end

def on_rollback(only: [:create, :update, :destroy], &blk)
Array(only).each do |verb|
config[:on_rollback][verb] = blk
end
end

def attribute(name, type, options = {}, &blk)
raise Errors::TypeNotFound.new(self, name, type) unless Types[type]
attribute_option(options, :readable)
Expand Down
39 changes: 31 additions & 8 deletions lib/graphiti/util/hooks.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,31 +2,54 @@ module Graphiti
module Util
class Hooks
def self.record
self.hooks = []
self.reset!
begin
yield.tap { run }
ensure
self.hooks = []
self.reset!
end
end

def self._hooks
Thread.current[:_graphiti_hooks] ||= []
Thread.current[:_graphiti_hooks] || self.reset!
end
private_class_method :_hooks

def self.hooks=(val)
Thread.current[:_graphiti_hooks] = val
def self.reset!
Thread.current[:_graphiti_hooks] = {
before_commit: [],
rollback: [],
post_process: [],
staged_rollbacks: []
}
end

# Because hooks will be added from the outer edges of
# the graph, working inwards
def self.add(prc)
_hooks.unshift(prc)
def self.add(before_commit, rollback)
_hooks[:before_commit].unshift(before_commit)
_hooks[:rollback].unshift(rollback)
end

def self.add_post_process(prc)
_hooks[:post_process].unshift(prc)
end

def self.run
_hooks.each { |h| h.call }
begin
_hooks[:before_commit].each_with_index do |before_commit, idx|
result = before_commit.call
rollback = _hooks[:rollback][idx]

# Want to run rollbacks in reverse order from before_commit hooks
_hooks[:staged_rollbacks].unshift(-> { rollback.call(result) })
end
rescue => e
_hooks[:staged_rollbacks].each {|h| h.call }
raise e
end

_hooks[:post_process].each {|h| h.call }
end
end
end
Expand Down
15 changes: 11 additions & 4 deletions lib/graphiti/util/persistence.rb
Original file line number Diff line number Diff line change
Expand Up @@ -61,15 +61,22 @@ def run

post_process(persisted, parents)
post_process(persisted, children)

before_commit = -> { @resource.before_commit(persisted, @meta[:method]) }
add_hook(before_commit)
on_rollback = ->(record) { @resource.on_rollback(record, @meta[:method]) }
add_hooks(before_commit, on_rollback)

persisted
end

private

def add_hook(prc)
::Graphiti::Util::Hooks.add(prc)
def add_hooks(bc, rb)
::Graphiti::Util::Hooks.add(bc, rb)
end

def add_post_process_hook(prc)
::Graphiti::Util::Hooks.add_post_process(prc)
end

# The child's attributes should be modified to nil-out the
Expand Down Expand Up @@ -185,7 +192,7 @@ def post_process(caller_model, processed)
group.group_by { |g| g[:sideload] }.each_pair do |sideload, members|
objects = members.map { |x| x[:object] }
hook = -> { sideload.fire_hooks!(caller_model, objects, method) }
add_hook(hook)
add_post_process_hook(hook)
end
end
end
Expand Down
5 changes: 1 addition & 4 deletions spec/integration/rails/before_commit_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,7 @@ def self.add(name, object)
end

before do
allow(controller.request.env).to receive(:[])
.with(anything).and_call_original
allow(controller.request.env).to receive(:[])
.with('PATH_INFO') { path }
allow(controller.request).to receive(:env).and_return(Rack::MockRequest.env_for(path))
end

let(:path) { '/integration_hooks/employees' }
Expand Down
205 changes: 205 additions & 0 deletions spec/integration/rails/rollback_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
if ENV["APPRAISAL_INITIALIZED"]
RSpec.describe 'rollback hooks', type: :controller do
class Callbacks
class << self
attr_accessor :rollbacks, :commits
end

def self.add_rollback(object)
self.rollbacks << object
end

def self.add_commit(object)
self.commits << object
end
end

before do
Callbacks.rollbacks = []
Callbacks.commits = []
$raise_on_before_commit = { }
end

before do
allow(controller.request).to receive(:env).and_return(Rack::MockRequest.env_for(path))
end

let(:path) { '/integration_hooks/employees' }

module IntegrationHooks
class ApplicationResource < Graphiti::Resource
self.adapter = Graphiti::Adapters::ActiveRecord
before_commit do |record|
Callbacks.add_commit(record)

if $raise_on_before_commit[record.class.name]
raise 'rollitback'
end

record
end
end

class DepartmentResource < ApplicationResource
self.model = ::Department

on_rollback do |record|
Callbacks.add_rollback(record)
end
end

class PositionResource < ApplicationResource
self.model = ::Position

attribute :employee_id, :integer, only: [:writable]

on_rollback do |record|
Callbacks.add_rollback(record)
end

belongs_to :department
end

class EmployeeResource < ApplicationResource
self.model = ::Employee

attribute :first_name, :string

on_rollback only: [:create] do |record|
Callbacks.add_rollback(record)
end

has_many :positions
end
end

controller(ApplicationController) do
def create
employee = IntegrationHooks::EmployeeResource.build(params)

if employee.save
render jsonapi: employee
else
raise 'whoops'
end
end

private

def params
@params ||= begin
hash = super.to_unsafe_h.with_indifferent_access
hash = hash[:params] if hash.has_key?(:params)
hash
end
end
end

before do
@request.headers['Accept'] = Mime[:json]
@request.headers['Content-Type'] = Mime[:json].to_s

routes.draw {
post "create" => "anonymous#create"
put "update" => "anonymous#update"
delete "destroy" => "anonymous#destroy"
}
end

def json
JSON.parse(response.body)
end

context 'on_rollback' do
context 'when creating a single resource' do
let(:payload) do
{
data: {
type: 'employees',
attributes: { first_name: 'Jane' }
}
}
end

context 'when the creation is successful' do
it "does not call rollback hook" do
post :create, params: payload

expect(Callbacks.rollbacks).to eq []
end
end

context 'when the resource raises an error in before_commit' do
before do
$raise_on_before_commit = { 'Employee' => true }
end

it "does not call rollback hook" do
expect {
post :create, params: payload
}.to raise_error('rollitback')

expect(Callbacks.rollbacks).to eq []
end
end
end

context 'creating nested resources' do
let(:payload) do
{
data: {
type: 'employees',
attributes: { first_name: 'joe' },
relationships: {
positions: {
data: [
{ :'temp-id' => 'a', type: 'positions', method: 'create' }
]
}
}
},
included: [
{
type: 'positions',
:'temp-id' => 'a',
relationships: {
department: {
data: {
:'temp-id' => 'b', type: 'departments', method: 'create'
}
}
}
},
{
type: 'departments',
:'temp-id' => 'b'
}
]
}
end

context 'when creation is successful' do
it 'does not call any rollback hooks' do
post :create, params: payload

expect(Callbacks.rollbacks).to eq []
end
end

context 'when one of the resources throws an error in a before_commit hook' do
before do
$raise_on_before_commit = { 'Department' => true }
end

it 'runs rollback hook for any previously commited resources in reverse order' do
expect {
post :create, params: payload
}.to raise_error('rollitback')

expect(Callbacks.rollbacks).to eq [Callbacks.commits[1], Callbacks.commits[0]]
end
end
end
end
end
end
Loading