Skip to content

Commit 5f65c15

Browse files
authored
Merge pull request #153 from A-Boudi/122-add-before-validation-hook
Add before_validation hooks to resources
2 parents d6d9f69 + 38d4b72 commit 5f65c15

File tree

9 files changed

+154
-4
lines changed

9 files changed

+154
-4
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,11 @@ Fixes:
4646

4747
### master (unreleased)
4848

49+
Features:
50+
51+
- [#153](https://github.com/graphiti-api/graphiti/pull/153) Add after_graph_persist hook.
52+
This hook fires after the graph of resources is persisted and before validation. (@A-Boudi)
53+
4954
<!-- ### [version (YYYY-MM-DD)](diff_link) -->
5055
<!-- Breaking changes:-->
5156
<!-- Features:-->

lib/graphiti/resource.rb

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,13 @@ def resolve(scope)
112112
adapter.resolve(scope)
113113
end
114114

115+
def after_graph_persist(model, metadata)
116+
hooks = self.class.config[:after_graph_persist][metadata[:method]] || []
117+
hooks.each do |hook|
118+
instance_exec(model, metadata, &hook)
119+
end
120+
end
121+
115122
def before_commit(model, metadata)
116123
hooks = self.class.config[:before_commit][metadata[:method]] || []
117124
hooks.each do |hook|

lib/graphiti/resource/configuration.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,7 @@ def config
190190
sort_all: nil,
191191
sorts: {},
192192
pagination: nil,
193+
after_graph_persist: {},
193194
before_commit: {},
194195
after_commit: {},
195196
attributes: {},

lib/graphiti/resource/dsl.rb

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,13 @@ def default_filter(name = nil, &blk)
8282
}
8383
end
8484

85+
def after_graph_persist(only: [:create, :update, :destroy], &blk)
86+
Array(only).each do |verb|
87+
config[:after_graph_persist][verb] ||= []
88+
config[:after_graph_persist][verb] << blk
89+
end
90+
end
91+
8592
def before_commit(only: [:create, :update, :destroy], &blk)
8693
Array(only).each do |verb|
8794
config[:before_commit][verb] ||= []

lib/graphiti/resource_proxy.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ def destroy
121121
metadata = {method: :destroy}
122122
model = @resource.destroy(@query.filters[:id], metadata)
123123
model.instance_variable_set(:@__serializer_klass, @resource.serializer)
124+
@resource.after_graph_persist(model, metadata)
124125
validator = ::Graphiti::Util::ValidationResponse.new \
125126
model, @payload
126127
validator.validate!
@@ -161,6 +162,7 @@ def persist
161162
transaction_response = @resource.transaction do
162163
::Graphiti::Util::TransactionHooksRecorder.record do
163164
model = yield
165+
::Graphiti::Util::TransactionHooksRecorder.run_graph_persist_hooks
164166
validator = ::Graphiti::Util::ValidationResponse.new \
165167
model, @payload
166168
validator.validate!

lib/graphiti/util/persistence.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@ def run
6060

6161
post_process(persisted, parents)
6262
post_process(persisted, children)
63+
after_graph_persist = -> { @resource.after_graph_persist(persisted, metadata) }
64+
add_hook(after_graph_persist, :after_graph_persist)
6365
before_commit = -> { @resource.before_commit(persisted, metadata) }
6466
add_hook(before_commit, :before_commit)
6567
after_commit = -> { @resource.after_commit(persisted, metadata) }

lib/graphiti/util/transaction_hooks_recorder.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,10 @@ def record
4141
end
4242
end
4343

44+
def run_graph_persist_hooks
45+
run(:after_graph_persist)
46+
end
47+
4448
# Because hooks will be added from the outer edges of
4549
# the graph, working inwards
4650
def add(prc, lifecycle_event)
@@ -59,6 +63,7 @@ def _hooks
5963

6064
def reset_hooks
6165
Thread.current[:_graphiti_hooks] = {
66+
after_graph_persist: [],
6267
before_commit: [],
6368
after_commit: [],
6469
}

spec/integration/rails/persistence_spec.rb

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,94 @@
276276
end
277277
end
278278

279+
describe "after graph persisted validation" do
280+
subject(:make_request) { do_update(payload) }
281+
282+
let(:klass) do
283+
Class.new(EmployeeResource) do
284+
self.validate_endpoints = false
285+
286+
after_graph_persist do |model|
287+
model.valid?(:after_graph_persisted)
288+
end
289+
end
290+
end
291+
292+
let(:polyvalentEmployee) do
293+
Class.new(Employee) do
294+
def self.model_name
295+
ActiveModel::Name.new(self, nil, 'PolyvalentEmployee')
296+
end
297+
validates :positions, length: { minimum: 2, too_short: 'too short, minimum is 2' }, on: :after_graph_persisted
298+
end
299+
end
300+
301+
let(:employee) { Employee.create!(first_name: "Jane") }
302+
303+
let(:payload) do
304+
{
305+
data: {
306+
type: "employees",
307+
id: employee.id.to_s,
308+
relationships: {
309+
positions: {
310+
data: [
311+
{ 'temp-id': "pos1", type: "positions", method: "create" },
312+
{ 'temp-id': "pos2", type: "positions", method: "create" },
313+
],
314+
},
315+
},
316+
},
317+
included: [
318+
{
319+
'temp-id': "pos1",
320+
type: "positions",
321+
attributes: {title: "foo"},
322+
},
323+
{
324+
'temp-id': "pos2",
325+
type: "positions",
326+
attributes: {title: "bar"},
327+
},
328+
],
329+
}
330+
end
331+
332+
before do
333+
klass.model = polyvalentEmployee
334+
allow(controller).to receive(:resource) { klass }
335+
end
336+
337+
context "when valid" do
338+
it "responds with the persisted data" do
339+
make_request
340+
expect(jsonapi_included.count).to eq(2)
341+
expect(jsonapi_included.map { |inc| inc.attributes["title"] }).to eq(["foo", "bar"])
342+
end
343+
end
344+
345+
context "when validation error" do
346+
before do
347+
payload[:data][:relationships][:positions][:data].pop
348+
end
349+
350+
it "returns validation error response" do
351+
make_request
352+
expect(json["errors"].first).to match(
353+
"code" => "unprocessable_entity",
354+
"status" => "422",
355+
"source" => {"pointer" => "/data/relationships/positions"},
356+
"detail" => "Positions too short, minimum is 2",
357+
"title" => "Validation Error",
358+
"meta" => hash_including(
359+
"attribute" => "positions",
360+
"message" => "too short, minimum is 2"
361+
)
362+
)
363+
end
364+
end
365+
end
366+
279367
describe "non-writable foreign keys" do
280368
subject(:make_request) { do_update(payload) }
281369

spec/integration/rails/transaction_hooks_spec.rb

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# rubocop: disable Style/GlobalVars
22

33
if ENV["APPRAISAL_INITIALIZED"]
4-
RSpec.describe "before_ & after_commit hooks", type: :controller do
4+
RSpec.describe "after_graph_persist, before_ & after_commit hooks", type: :controller do
55
class Callbacks
66
class << self
77
attr_accessor :fired, :in_transaction_during, :entities
@@ -27,6 +27,7 @@ def in_transaction?
2727
Callbacks.fired = {}
2828
Callbacks.in_transaction_during = {}
2929
Callbacks.entities = []
30+
$raise_on_after_graph_persist = {employee: false}
3031
$raise_on_before_commit = {employee: true}
3132
end
3233

@@ -63,7 +64,7 @@ class PositionResource < ApplicationResource
6364
before_commit do |position|
6465
Callbacks.add(:before_position, position)
6566
if $raise_on_before_commit[:position]
66-
raise "rollitback_book"
67+
raise "rollitback_position"
6768
end
6869
end
6970

@@ -75,6 +76,13 @@ class EmployeeResource < ApplicationResource
7576

7677
attribute :first_name, :string
7778

79+
after_graph_persist do |employee|
80+
Callbacks.add(:after_graph_persist, employee)
81+
if $raise_on_after_graph_persist[:employee]
82+
raise "rollitback"
83+
end
84+
end
85+
7886
before_commit only: [:create] do |employee|
7987
Callbacks.add(:before_create, employee)
8088
if $raise_on_before_commit[:employee]
@@ -191,7 +199,7 @@ def json
191199
post :create, params: payload
192200
}.to raise_error("rollitback")
193201
expect(Employee.count).to be_zero
194-
expect(Callbacks.entities.length).to eq(1)
202+
expect(Callbacks.entities.length).to eq(2)
195203
expect(Callbacks.fired[:before_create]).to be_a(Employee)
196204
end
197205

@@ -218,7 +226,7 @@ def json
218226
expect {
219227
post :create, params: payload
220228
}.to raise_error("whoops")
221-
expect(Callbacks.entities).to be_empty
229+
expect(Callbacks.entities).to include(:after_graph_persist)
222230
end
223231
end
224232

@@ -230,13 +238,31 @@ def json
230238
it "fires all before and after_commit hooks" do
231239
post :create, params: payload
232240
expect(Callbacks.entities).to eq([
241+
:after_graph_persist,
233242
:before_create,
234243
:stacked_before_create,
235244
:employee_after_create,
236245
:employee_after_create_eval_test,
237246
])
238247
end
239248
end
249+
250+
context "when an error is raised after_graph_persist" do
251+
before do
252+
$raise_on_after_graph_persist = {employee: true}
253+
end
254+
255+
it "does not run before_commit callbacks" do
256+
expect_any_instance_of(Graphiti::Util::ValidationResponse)
257+
.to_not receive(:validate!)
258+
expect {
259+
post :create, params: payload
260+
}.to raise_error("rollitback")
261+
expect(Employee.count).to be_zero
262+
expect(Callbacks.entities.length).to eq(1)
263+
expect(Callbacks.fired[:after_graph_persist]).to be_a(Employee)
264+
end
265+
end
240266
end
241267

242268
context "nested" do
@@ -281,6 +307,7 @@ def json
281307
post :create, params: payload
282308

283309
expect(Callbacks.entities).to eq([
310+
:after_graph_persist,
284311
:before_create,
285312
:stacked_before_create,
286313
:before_position,
@@ -309,6 +336,12 @@ def json
309336
expect(Callbacks.in_transaction_during[:employee_after_create]).to eq false
310337
expect(Callbacks.fired[:employee_after_create_eval_test]).to be_a(IntegrationHooks::EmployeeResource)
311338
end
339+
340+
it "can access children resources from after_graph_persist" do
341+
post :create, params: payload
342+
expect(Callbacks.fired[:employee_after_create].positions.length).to eq(1)
343+
expect(Callbacks.fired[:employee_after_create].positions[0]).to be_a(Position)
344+
end
312345
end
313346

314347
context "when yielding meta" do

0 commit comments

Comments
 (0)