Skip to content

Commit 11358b7

Browse files
committed
Provide access to underlying model during build/update/save process, closer mirroring rails controller patterns.
Changes: - (for creates): @resource = MyResource.build(params); @resource.data #=> unsaved model with attributes applied - (for updates): @resource = MyResource.find(params); @resource.assign_attributes @resource.data #=> unsaved model with attributes applied
1 parent 764eb93 commit 11358b7

File tree

10 files changed

+129
-7
lines changed

10 files changed

+129
-7
lines changed

lib/graphiti/request_validators/validator.rb

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ def validate
1717
return true unless @params.has_key?(:data)
1818

1919
resource = @root_resource
20-
2120
if @params[:data].has_key?(:type)
2221
if (meta_type = deserialized_payload.meta[:type].try(:to_sym))
2322
if @root_resource.type != meta_type && @root_resource.polymorphic?

lib/graphiti/resource.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,12 @@ def disassociate(parent, child, association_name, type)
9797
adapter.disassociate(parent, child, association_name, type)
9898
end
9999

100+
def assign_with_relationships(meta, attributes, relationships, caller_model = nil, foreign_key = nil)
101+
persistence = Graphiti::Util::Persistence \
102+
.new(self, meta, attributes, relationships, caller_model, foreign_key)
103+
persistence.assign
104+
end
105+
100106
def persist_with_relationships(meta, attributes, relationships, caller_model = nil, foreign_key = nil)
101107
persistence = Graphiti::Util::Persistence \
102108
.new(self, meta, attributes, relationships, caller_model, foreign_key)

lib/graphiti/resource/interface.rb

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,9 @@ def _find(params = {}, base_scope = nil)
4949
def build(params, base_scope = nil)
5050
validate_request!(params)
5151
runner = Runner.new(self, params)
52-
runner.proxy(base_scope, single: true, raise_on_missing: true)
52+
runner.proxy(base_scope, single: true, raise_on_missing: true).tap do |instance|
53+
instance.assign_attributes # assign the params to the underlying model
54+
end
5355
end
5456

5557
private

lib/graphiti/resource/persistence.rb

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,24 @@ def add_callback(kind, lifecycle, method, only, &blk)
6969
end
7070
end
7171

72+
def assign(assign_params, meta = nil)
73+
id = assign_params[:id]
74+
assign_params = assign_params.except(:id)
75+
model_instance = nil
76+
77+
run_callbacks :attributes, :assign, assign_params, meta do |params|
78+
model_instance = if meta[:method] == :update && id
79+
self.class._find(id: id).data
80+
else
81+
call_with_meta(:build, model, meta)
82+
end
83+
call_with_meta(:assign_attributes, model_instance, params, meta)
84+
model_instance
85+
end
86+
87+
model_instance
88+
end
89+
7290
def create(create_params, meta = nil)
7391
model_instance = nil
7492

lib/graphiti/resource_proxy.rb

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,16 @@ def pagination
117117
@pagination ||= Delegates::Pagination.new(self)
118118
end
119119

120+
def assign_attributes(params = nil)
121+
# deserialize params again?
122+
123+
@data = @resource.assign_with_relationships(
124+
@payload.meta,
125+
@payload.attributes,
126+
@payload.relationships,
127+
)
128+
end
129+
120130
def save(action: :create)
121131
# TODO: remove this. Only used for persisting many-to-many with AR
122132
# (see activerecord adapter)
@@ -167,6 +177,7 @@ def destroy
167177

168178
def update
169179
resolve_data
180+
assign_attributes
170181
save(action: :update)
171182
end
172183

lib/graphiti/runner.rb

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ def initialize(resource_class, params, query = nil, action = nil)
88
@params = params
99
@query = query
1010
@action = action
11-
1211
validator = RequestValidator.new(jsonapi_resource, params, action)
1312
validator.validate!
1413

lib/graphiti/util/persistence.rb

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,14 @@ def initialize(resource, meta, attributes, relationships, caller_model, foreign_
2424
end
2525
end
2626

27+
def assign
28+
attributes = @adapter.persistence_attributes(self, @attributes)
29+
@assigned = call_resource_method(:assign, attributes, @caller_model)
30+
@resource.decorate_record(@assigned)
31+
32+
@assigned
33+
end
34+
2735
# Perform the actual save logic.
2836
#
2937
# belongs_to must be processed before/separately from has_many -
@@ -49,8 +57,10 @@ def run
4957
parents = @adapter.process_belongs_to(self, attributes)
5058
persisted = persist_object(@meta[:method], attributes)
5159
@resource.decorate_record(persisted)
52-
assign_temp_id(persisted, @meta[:temp_id])
5360

61+
return persisted if @meta[:method] == :assign
62+
63+
assign_temp_id(persisted, @meta[:temp_id])
5464
associate_parents(persisted, parents)
5565

5666
children = @adapter.process_has_many(self, persisted)
@@ -129,6 +139,8 @@ def associate_children(object, children)
129139

130140
def persist_object(method, attributes)
131141
case method
142+
when :assign
143+
call_resource_method(:assign, attributes, @caller_model)
132144
when :destroy
133145
call_resource_method(:destroy, attributes[:id], @caller_model)
134146
when :update, nil, :disassociate

spec/integration/rails/callbacks_spec.rb

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
routes.draw do
1010
post "create" => "anonymous#create"
11+
put "update" => "anonymous#update"
1112
delete "destroy" => "anonymous#destroy"
1213
end
1314

@@ -34,6 +35,7 @@ class ApplicationResource < Graphiti::Resource
3435

3536
class EmployeeResource < ApplicationResource
3637
self.model = Employee
38+
self.type= "employees"
3739

3840
before_attributes :one
3941
before_attributes :two
@@ -168,6 +170,18 @@ def create
168170
end
169171
end
170172

173+
def update
174+
employee = IntegrationCallbacks::EmployeeResource._find(params)
175+
Thread.current[:proxy] = employee
176+
employee.assign_attributes
177+
178+
if employee.update_attributes
179+
render jsonapi: employee
180+
else
181+
raise "whoops"
182+
end
183+
end
184+
171185
def destroy
172186
employee = IntegrationCallbacks::EmployeeResource._find(params)
173187
Thread.current[:proxy] = employee
@@ -194,7 +208,7 @@ def params
194208
{
195209
data: {
196210
type: "employees",
197-
attributes: {first_name: "Jane"}
211+
attributes: { first_name: "Jane"}
198212
}
199213
}
200214
end
@@ -227,6 +241,41 @@ def params
227241
end
228242
end
229243

244+
describe "update callbacks" do
245+
let!(:employee) { Employee.create!(first_name: "asdf") }
246+
let(:payload) {
247+
{ id: employee.id,
248+
data: {
249+
id: employee.id,
250+
type: 'employees',
251+
attributes: { first_name: "Jane" }
252+
}
253+
}
254+
}
255+
256+
257+
it "fires hooks in order" do
258+
expect {
259+
put :update, params: payload
260+
}.to change { Employee.find(employee.id).first_name }
261+
employee = proxy.data
262+
expect(employee.first_name)
263+
.to eq("Jane5a6a7a12347b6b5b_12a_13a_14a89_10_11_14b_13b_12b")
264+
end
265+
266+
context "when an error is raised" do
267+
before do
268+
$raise = true
269+
end
270+
271+
it "rolls back the transaction" do
272+
expect {
273+
expect { put :update, params: payload }.to raise_error("test")
274+
}.to_not(change { Employee.count })
275+
end
276+
end
277+
end
278+
230279
describe "destroy callbacks" do
231280
let!(:employee) { Employee.create!(first_name: "Jane") }
232281

spec/persistence_spec.rb

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,22 @@ def expect_errors(object, expected)
3737
expect(employee.data.first_name).to eq("Jane")
3838
end
3939

40+
it "can access the unsaved model after build" do
41+
employee = klass.build(payload)
42+
expect(employee.data).to_not be_nil
43+
expect(employee.data.first_name).to eq("Jane")
44+
expect(employee.data.id).to be_nil
45+
end
46+
47+
xit "can modify attributes directly on the unsaved model before save" do
48+
employee = klass.build(payload)
49+
expect(employee.data).to_not be_nil
50+
employee.data.first_name = "June"
51+
52+
expect(employee.save).to eq(true)
53+
expect(employee.data.first_name).to eq("June")
54+
end
55+
4056
describe "updating" do
4157
let!(:employee) { PORO::Employee.create(first_name: "asdf") }
4258

@@ -52,6 +68,16 @@ def expect_errors(object, expected)
5268
}.to raise_error(Graphiti::Errors::RecordNotFound)
5369
end
5470
end
71+
72+
it "can apply attributes and access model" do
73+
employee = klass.find(payload)
74+
expect(employee.data.first_name).to eq("asdf")
75+
employee.assign_attributes
76+
expect(employee.data.first_name).to eq("Jane")
77+
78+
employee = klass.find(payload)
79+
expect(employee.data.first_name).to eq("asdf")
80+
end
5581
end
5682

5783
describe "destroying" do
@@ -1825,8 +1851,8 @@ def delete(model, meta)
18251851

18261852
context "and it is a create operation" do
18271853
it "works" do
1828-
instance = klass.build(payload)
18291854
expect {
1855+
instance = klass.build(payload)
18301856
instance.save
18311857
}.to raise_error(Graphiti::Errors::InvalidRequest, /data.attributes.id/)
18321858
end

spec/support/rails/employee_controller.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ def create
1515

1616
def update
1717
employee = resource.find(params)
18-
18+
1919
if employee.update_attributes
2020
render jsonapi: employee
2121
else

0 commit comments

Comments
 (0)