Skip to content

Commit 05b0ec3

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 4cad908 commit 05b0ec3

File tree

10 files changed

+143
-20
lines changed

10 files changed

+143
-20
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: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,16 @@ 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(params) # assign the params to the underlying model
54+
end
55+
end
56+
57+
def load(models, base_scope = nil)
58+
runner = Runner.new(self, {}, base_scope, :find)
59+
runner.proxy(nil, bypass_required_filters: true).tap do |r|
60+
r.data = models
61+
end
5362
end
5463

5564
private

lib/graphiti/resource/persistence.rb

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@ module Persistence
44
extend ActiveSupport::Concern
55

66
class_methods do
7-
def before_attributes(method = nil, only: [:create, :update], &blk)
7+
def before_attributes(method = nil, only: [:create, :update, :assign], &blk)
88
add_callback(:attributes, :before, method, only, &blk)
99
end
1010

11-
def after_attributes(method = nil, only: [:create, :update], &blk)
11+
def after_attributes(method = nil, only: [:create, :update, :assign], &blk)
1212
add_callback(:attributes, :after, method, only, &blk)
1313
end
1414

@@ -69,15 +69,29 @@ def add_callback(kind, lifecycle, method, only, &blk)
6969
end
7070
end
7171

72+
def assign(assign_params, meta = nil, action_name = nil)
73+
id = assign_params[:id]
74+
assign_params = assign_params.except(:id)
75+
model_instance = nil
76+
77+
run_callbacks :attributes, action_name, assign_params, meta do |params|
78+
model_instance = if action_name != :create && 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

7593
run_callbacks :persistence, :create, create_params, meta do
76-
run_callbacks :attributes, :create, create_params, meta do |params|
77-
model_instance = call_with_meta(:build, model, meta)
78-
call_with_meta(:assign_attributes, model_instance, params, meta)
79-
model_instance
80-
end
94+
model_instance = assign(create_params, meta, :create)
8195

8296
run_callbacks :save, :create, model_instance, meta do
8397
model_instance = call_with_meta(:save, model_instance, meta)
@@ -89,15 +103,9 @@ def create(create_params, meta = nil)
89103

90104
def update(update_params, meta = nil)
91105
model_instance = nil
92-
id = update_params[:id]
93-
update_params = update_params.except(:id)
94106

95107
run_callbacks :persistence, :update, update_params, meta do
96-
run_callbacks :attributes, :update, update_params, meta do |params|
97-
model_instance = self.class._find(id: id).data
98-
call_with_meta(:assign_attributes, model_instance, params, meta)
99-
model_instance
100-
end
108+
model_instance = assign(update_params, meta, :update)
101109

102110
run_callbacks :save, :update, model_instance, meta do
103111
model_instance = call_with_meta(:save, model_instance, meta)

lib/graphiti/resource_proxy.rb

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ def initialize(resource, scope, query,
88
payload: nil,
99
single: false,
1010
raise_on_missing: false,
11+
data: nil,
1112
cache: nil,
1213
cache_expires_in: nil)
1314

@@ -74,6 +75,11 @@ def as_graphql(options = {})
7475
Renderer.new(self, options).as_graphql
7576
end
7677

78+
def data=(models)
79+
@data = data
80+
[@data].flatten.compact.each { |r| @resource.decorate_record(r) }
81+
end
82+
7783
def data
7884
@data ||= begin
7985
records = @scope.resolve
@@ -84,6 +90,7 @@ def data
8490
records
8591
end
8692
end
93+
8794
alias_method :to_a, :data
8895
alias_method :resolve_data, :data
8996

@@ -117,6 +124,16 @@ def pagination
117124
@pagination ||= Delegates::Pagination.new(self)
118125
end
119126

127+
def assign_attributes(params = nil)
128+
# deserialize params again?
129+
130+
@data = @resource.assign_with_relationships(
131+
@payload.meta,
132+
@payload.attributes,
133+
@payload.relationships
134+
)
135+
end
136+
120137
def save(action: :create)
121138
# TODO: remove this. Only used for persisting many-to-many with AR
122139
# (see activerecord adapter)
@@ -170,7 +187,7 @@ def update
170187
save(action: :update)
171188
end
172189

173-
alias update_attributes update # standard:disable Style/Alias
190+
alias_method :update_attributes, :update
174191

175192
def include_hash
176193
@include_hash ||= begin

lib/graphiti/runner.rb

Lines changed: 1 addition & 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

@@ -72,6 +71,7 @@ def proxy(base = nil, opts = {})
7271
payload: deserialized_payload,
7372
single: opts[:single],
7473
raise_on_missing: opts[:raise_on_missing],
74+
data: opts[:data],
7575
cache: opts[:cache],
7676
cache_expires_in: opts[:cache_expires_in]
7777
end

lib/graphiti/serializer.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ def strip_relationships!(hash)
100100
def strip_relationships?
101101
return false unless Graphiti.config.links_on_demand
102102
params = Graphiti.context[:object]&.params || {}
103+
103104
[false, nil, "false"].include?(params[:links])
104105
end
105106
end

lib/graphiti/util/persistence.rb

Lines changed: 11 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 = @resource.assign(attributes, @meta, :assign)
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,8 @@ 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+
assign_temp_id(persisted, @meta[:temp_id])
5462
associate_parents(persisted, parents)
5563

5664
children = @adapter.process_has_many(self, persisted)
@@ -129,6 +137,8 @@ def associate_children(object, children)
129137

130138
def persist_object(method, attributes)
131139
case method
140+
when :assign
141+
call_resource_method(:assign, attributes, @caller_model)
132142
when :destroy
133143
call_resource_method(:destroy, attributes[:id], @caller_model)
134144
when :update, nil, :disassociate

spec/integration/rails/callbacks_spec.rb

Lines changed: 47 additions & 0 deletions
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
@@ -227,6 +241,39 @@ 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+
it "fires hooks in order" do
256+
expect {
257+
put :update, params: payload
258+
}.to change { Employee.find(employee.id).first_name }
259+
employee = proxy.data
260+
expect(employee.first_name)
261+
.to eq("Jane5a6a7a12347b6b5b_12a_13a_14a89_10_11_14b_13b_12b")
262+
end
263+
264+
context "when an error is raised" do
265+
before do
266+
$raise = true
267+
end
268+
269+
it "rolls back the transaction" do
270+
expect {
271+
expect { put :update, params: payload }.to raise_error("test")
272+
}.to_not(change { Employee.count })
273+
end
274+
end
275+
end
276+
230277
describe "destroy callbacks" do
231278
let!(:employee) { Employee.create!(first_name: "Jane") }
232279

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

0 commit comments

Comments
 (0)