Closed derekcannon closed 6 years ago
Hi Derek - great question. I think I have an answer for you, the documentation just hasn't caught up yet. In your example, you could do something like this:
class AddressResource < JsonapiCompliable::Resource
# ... code ...
def create(attributes, parent) # note the optional second arg here
address = Address.new(attributes)
if parent.is_a?(Order)
address.geocode
.time_in_new_timezone(parent.some_time, address.timezone)
end
address.save
address
end
end
Note the check on parent
here - since multiple objects could (in theory) sidepost an address, we should verify it's an Order
doing the sideposting.
Another option would be the after_save
hooks within the parent. I haven't written a how-to yet, but you can see examples in the tests. One way:
# OrderResource
belongs_to :address,
foreign_key: :order_id,
scope: -> { Address.all },
resource: AddressResource do
after_save only: [:create, update] do |order, addresses|
# addresses is always an array regardless of has-many/belongs-to
address = addresses.first
address.geocode(order.some_time, address.timezone)
address.save
end
For both of these examples, you'll already be within a transaction. Make sure to define #transaction
in your adapter if not using activerecord.
Does either of those work for you?
And by the way, as you noted earlier - another option is to simply post a CompoundOrder
object, rather than sideposting the address. In this case you'd have a CompoundOrder
resource and the attributes
could be whatever structure you want, and you could do whatever you want within the relevant resource methods. I think this is less than ideal, but it's what a lot of RESTful folks did/do before jsonapi introduced sideposting, so no worse than the status quo as a last resort.
Thanks for the quick reply! Having access to the parent is definitely helpful. I want to avoid callbacks as much as possible; it's gotten us into a lot of trouble in our past API.
I am thinking at this point the compound object would be best. Ideally, I'd like to define a central place where things are happening to an order, so it's easy to see what effects are taking place.
Another option, for future consideration, might be the ability to specify which resource handles the create/update for a relationship. Something like:
# app/resources/order_resource.rb
class OrderResource < ApplicationResource
type :orders
model Order
has_one :address,
resource: AddressResource,
persisted_by: :self, # something to specify OrderResource is handling address params
scope: -> { Address.all },
foreign_key: :address_id
end
This way, AddressResource
can operate as normal in any situation and not know about the custom requirements for OrderResource
, while OrderResource
is smart enough to handle address params in a certain way. What do you think?
I'm definitely open to more options/injectables around persistence. That said, I'm not sure I agree with that abstraction - I like the premise of an OrderResource
persists an Order
, an AddressResource
persists an Address
, and a CompoundOrderResource
is persisted by a CompoundOrderService
that coordinates multiple resources. Cross-pollinating these sounds like we'd end up with some confusing "who does what where" issues, and some "unexpected side-effect" issues similar to what you've seen before with callbacks.
I do think there is probably something to the idea of a JsonapiCompliable::CompoundResource
concept, but I'd like to hear more about these scenarios and problems with the existing paradigm before digging too deep. Maybe you could start down the CompoundOrderResource
route and let me know any problems you run into?
I've read over the examples for both custom persistence and sideposting. It seems like to add a service call in the mix, you would define a
create(attributes)
method in the resource. If you're sideposting, I suppose you'd define multiplecreate(attributes)
on the various resources.Assuming that's all correct, what if your service object coordinates multiple models being posted together, none of which can be saved until the service object touches all of them?
A more concrete example, let's say you have an
Order
and anAddress
and need to geocode to find the timezone, then apply that timezone to a time on the order:I know there are other ways to approach this particular example, but it's just a simple example representing a much more complex service.