lib/restforce/concerns/composite_api.rb



# frozen_string_literal: true

require 'restforce/concerns/verbs'

module Restforce
  module Concerns
    module CompositeAPI
      extend Restforce::Concerns::Verbs

      define_verbs :post

      def composite(all_or_none: false, collate_subrequests: false)
        subrequests = Subrequests.new(options)
        yield(subrequests)

        if subrequests.requests.length > 25
          raise ArgumentError, 'Cannot have more than 25 subrequests.'
        end

        properties = {
          compositeRequest: subrequests.requests,
          allOrNone: all_or_none,
          collateSubrequests: collate_subrequests
        }
        response = api_post('composite', properties.to_json)

        results = response.body['CompositeResponse']
        has_errors = results.any? { |result| result['HttpStatusCode'].digits.last == 4 }
        if all_or_none && has_errors
          last_error_index = results.rindex { |result| result['HttpStatusCode'] != 412 }
          last_error = results[last_error_index]
          raise CompositeAPIError, last_error['Body'][0]['errorCode']
        end

        results
      end

      def composite!(collate_subrequests: false, &block)
        composite(all_or_none: true, collate_subrequests: collate_subrequests, &block)
      end

      class Subrequests
        def initialize(options)
          @options = options
          @requests = []
        end
        attr_reader :options, :requests

        def create(sobject, reference_id, attrs)
          requests << {
            method: 'POST',
            url: composite_api_path(sobject),
            body: attrs,
            referenceId: reference_id
          }
        end

        def update(sobject, reference_id, attrs)
          id = attrs.fetch(attrs.keys.find { |k, _v| k.to_s.casecmp?('id') }, nil)
          raise ArgumentError, 'Id field missing from attrs.' unless id

          attrs_without_id = attrs.reject { |k, _v| k.to_s.casecmp?('id') }
          requests << {
            method: 'PATCH',
            url: composite_api_path("#{sobject}/#{id}"),
            body: attrs_without_id,
            referenceId: reference_id
          }
        end

        def destroy(sobject, reference_id, id)
          requests << {
            method: 'DELETE',
            url: composite_api_path("#{sobject}/#{id}"),
            referenceId: reference_id
          }
        end

        def upsert(sobject, reference_id, ext_field, attrs)
          raise ArgumentError, 'External id field missing.' unless ext_field

          ext_id = attrs.fetch(attrs.keys.find do |k, _v|
            k.to_s.casecmp?(ext_field.to_s)
          end, nil)
          raise ArgumentError, 'External id missing from attrs.' unless ext_id

          attrs_without_ext_id = attrs.reject { |k, _v| k.to_s.casecmp?(ext_field) }
          requests << {
            method: 'PATCH',
            url: composite_api_path("#{sobject}/#{ext_field}/#{ext_id}"),
            body: attrs_without_ext_id,
            referenceId: reference_id
          }
        end

        private

        def composite_api_path(path)
          "/services/data/v#{options[:api_version]}/sobjects/#{path}"
        end
      end
    end
  end
end