JSON API standard: how to associate data that isn't saved and has no id (yet)?

214 views Asked by At

In my rails app I send data to the react front end in JSON API standard format via the jsonapi-serializer gem. This works well for data coming from our database, but when we have failing form data, which is sent in the form of rails nested resources, sometimes this data needs to be returned with errors before the records are actually saved.

When new model data has failing validations, I need those errors to come back to my front end and have the associations intact. I've been trying to run them through jsonapi-serializer as well, but the records that don't have ids, don't make it into the relationships key. The problem is that failing new records don't have an id, and they come back to the front end un-associated.

My only solution to this so far is to manually shim fake temporary ids in there. I'm not sure if I'm missing something obvious. That id is removed on the front end for re-submission. My solution isn't perfect and has limitations and issues.

Is there a built-in way to do this or am I stuck with my own implementation? Is there some kind of alternate key I could use for this association?

For what it's worth, this is my implementation so far, just to clarify what I'm doing. It's far from perfect and a lot of code for what seems like it should be a problem with a built-in solution. It's based on trying to iterate over the object tree being serialized and seeing if a "fake" id is needed. This still chokes on some things that can be passed via include: []

class BaseSerializer
  module ShimIdConcerns
    extend ActiveSupport::Concern
      
    private
  
    # Very Important! Controls relationship of json api assocs in front end
    # The id that is returned to the front end needs to be smarter than simply
    # the actual id or nil. The id is used to associate jsonapi objects to their
    # main resource whether they have an id yet or not. Sometimes these objects 
    # are not persisted, but have errors and need to be associated properly in the UI. 
    # Works in conjuncton with attribute :is_persisted
    def shim_id_on_failing_new_associations(resource, options)
      shim_id_on_failing_new_associations_recursive resource, options[:include].to_a
    end

    # dot_assocs: expects assocs in 'packages.package_services.service' format
    def shim_id_on_failing_new_associations_recursive(resource, dot_assocs)
      assocs = simplify_assocs(dot_assocs)

      assocs.each do |assoc_path|
        next if assoc_path.blank?
        segments = assoc_path.split('.') 
        method = segments.shift
        next unless resource.respond_to?(method)
        assoc = resource.send(method)

        if assoc.is_a?  ActiveRecord::Base
          shim_id(resource) 
          shim_id_on_failing_new_associations_recursive assoc, segments.join('.')
        elsif assoc.is_a?(ActiveRecord::Associations::CollectionProxy) || assoc.is_a?(Array)
          assoc.each do |one_of_many|
            shim_id_on_failing_new_associations_recursive one_of_many, segments.join('.')
          end
        end
      end
    end

    # Gives a temp id on failing or new resources so they can be associated in react
    # in jsonapi/react. Ensure this id is not re-submitted, however
    def shim_id(resource) 
      resource.id = rand(1000000000000) if resource.id.nil? && resource.new_record?
    end

    
    # turns
    #   [ :reservation, :'reservation.client', :'reservation.client.credit_card' ]
    # into
    #   [ "reservation.client.credit_card" ]
    # to avoid duplicate processing
    def simplify_assocs(dot_assocs)
      ap dot_assocs
      all = [dot_assocs].flatten.map(&:to_s)
      simp = []
      
      # yes quadratic, but will be little data
      all.each do |needle|
        matches = 0
        all.each do |hay|
          matches += 1 if hay.index(needle) === 0 
        end
        simp << needle if matches === 1
      end
      simp
    end
  end
end
0

There are 0 answers