class Attio::Internal::Record

@api private
Use Person, Company, or TypedRecord instead.
This class handles the complex Attio Record API and should not be used directly.
Base class for record-based resources (Person, Company, etc.)

def self.resource_path

Returns:
  • (String) - The API path
def self.resource_path
  "objects"
end

def add_to_list(list_id, **)

Add this record to a list
def add_to_list(list_id, **)
  list = List.retrieve(list_id, **)
  list.add_record(id, **)
end

def build_filter(filter)

def build_filter(filter)
  case filter
  when Hash
    filter
  when Array
    {"$and" => filter}
  else
    filter
  end
end

def build_query_params(params)

def build_query_params(params)
  query_params = {}
  query_params[:filter] = build_filter(params[:filter]) if params[:filter]
  query_params[:sort] = build_sort(params[:sort]) if params[:sort]
  query_params[:limit] = params[:limit] if params[:limit]
  query_params[:cursor] = params[:cursor] if params[:cursor]
  # Note: 'q' parameter is not supported by Attio API
  query_params
end

def build_sort(sort)

def build_sort(sort)
  case sort
  when String
    parse_sort_string(sort)
  when Hash
    sort
  else
    sort
  end
end

def create(object: nil, values: nil, data: nil, **opts)

Create a new record
def create(object: nil, values: nil, data: nil, **opts)
  # Handle both parameter styles
  if values
    # Test style: create(object: "people", values: {...})
    validate_object_identifier!(object)
    validate_values!(values)
    normalized_values = values
  elsif data && data[:values]
    # API style: create(object: "people", data: { values: {...} })
    validate_object_identifier!(object)
    validate_values!(data[:values])
    normalized_values = data[:values]
  else
    raise ArgumentError, "Must provide object and either values or data.values"
  end
  normalized = normalize_values(normalized_values)
  puts "DEBUG: Normalized values: #{normalized.inspect}" if ENV["ATTIO_DEBUG"]
  request_params = {
    data: {
      values: normalized
    }
  }
  response = execute_request(:POST, "#{resource_path}/#{object}/records", request_params, opts)
  # Ensure object info is included
  record_data = response["data"] || {}
  record_data["object_api_slug"] ||= object if record_data.is_a?(Hash)
  new(record_data, opts)
end

def destroy(**opts)

Override destroy to use correct path
def destroy(**opts)
  raise InvalidRequestError, "Cannot destroy a record without an ID" unless persisted?
  raise InvalidRequestError, "Cannot destroy without object context" unless object_api_slug
  self.class.send(:execute_request, :DELETE, resource_path, {}, opts)
  @attributes.clear
  @changed_attributes.clear
  @id = nil
  freeze
  true
end

def extract_single_value(value_data)

def extract_single_value(value_data)
  case value_data
  when Hash
    if value_data.key?(:value) || value_data.key?("value")
      value_data[:value] || value_data["value"]
    elsif value_data.key?(:target_object) || value_data.key?("target_object") ||
          value_data.key?(:referenced_actor_type) || value_data.key?("referenced_actor_type")
      # Reference value - return the full reference object
      value_data
    elsif value_data.key?(:currency_value) || value_data.key?("currency_value")
      # Currency value - return the full object to preserve currency info
      value_data
    elsif value_data.key?(:status) || value_data.key?("status")
      # Status value - return the full object to preserve status info
      value_data
    else
      value_data
    end
  else
    value_data
  end
end

def extract_value(value_data)

def extract_value(value_data)
  case value_data
  when Array
    extracted = value_data.map { |v| extract_single_value(v) }
    (extracted.length == 1) ? extracted.first : extracted
  else
    extract_single_value(value_data)
  end
end

def initialize(attributes = {}, opts = {})

def initialize(attributes = {}, opts = {})
  super
  normalized_attrs = normalize_attributes(attributes)
  @attio_object_id = normalized_attrs[:object_id]
  @object_api_slug = normalized_attrs[:object_api_slug]
  # Process values into attributes
  if normalized_attrs[:values]
    process_values(normalized_attrs[:values])
  end
end

def inspect

Returns:
  • (String) - Inspection string with ID, object, and sample values
def inspect
  values_preview = @attributes.take(3).map { |k, v| "#{k}: #{v.inspect}" }.join(", ")
  values_preview += "..." if @attributes.size > 3
  "#<#{self.class.name}:#{object_id} id=#{id.inspect} object=#{object_api_slug.inspect} values={#{values_preview}}>"
end

def list(object:, **opts)

List records for an object
def list(object:, **opts)
  validate_object_identifier!(object)
  # Extract query parameters from opts
  # Handle both opts[:params] (from find_by) and direct opts (from other callers)
  params = opts[:params] || opts
  query_params = build_query_params(params)
  response = execute_request(:POST, "#{resource_path}/#{object}/records/query", query_params, opts)
  APIResource::ListObject.new(response, self, opts.merge(object: object), opts)
end

def lists(**)

Get lists containing this record
def lists(**)
  raise InvalidRequestError, "Cannot get lists without an ID" unless persisted?
  # This is a simplified implementation - in reality you'd need to query the API
  # for lists that contain this record
  List.list(record_id: id, **)
end

def normalize_single_value(value)

def normalize_single_value(value)
  case value
  when Hash
    value
  when NilClass
    nil
  else
    {value: value}
  end
end

def normalize_values(values)

def normalize_values(values)
  values.map do |key, value|
    # Check if this is a simple array attribute
    if SIMPLE_ARRAY_ATTRIBUTES.include?(key.to_s) && value.is_a?(Array)
      # For email_addresses and domains, keep strings as-is
      [key, value]
    elsif SIMPLE_VALUE_ATTRIBUTES.include?(key.to_s) && !value.is_a?(Hash) && !value.is_a?(Array)
      # For simple string attributes, send directly
      [key, value]
    elsif OBJECT_ARRAY_ATTRIBUTES.include?(key.to_s) && value.is_a?(Array)
      # For arrays of objects like phone_numbers, etc., keep as-is
      [key, value]
    elsif key.to_s == "name"
      # Special handling for name - keep as-is whether string or array
      # Company names are strings, Person names are arrays of objects
      [key, value]
    else
      normalized_value = case value
      when Array
        value.map { |v| normalize_single_value(v) }
      else
        normalize_single_value(value)
      end
      [key, normalized_value]
    end
  end.to_h
end

def parse_sort_string(sort_string)

def parse_sort_string(sort_string)
  field, direction = sort_string.split(":")
  {
    field: field,
    direction: direction || "asc"
  }
end

def prepare_values_for_update

def prepare_values_for_update
  changed_attributes.transform_values do |value|
    self.class.send(:normalize_values, {key: value})[:key]
  end
end

def process_values(values)

def process_values(values)
  return unless values.is_a?(Hash)
  values.each do |key, value_data|
    extracted_value = extract_value(value_data)
    @attributes[key.to_sym] = extracted_value
    @original_attributes[key.to_sym] = deep_copy(extracted_value)
  end
end

def resource_path

def resource_path
  raise InvalidRequestError, "Cannot generate path without object context" unless object_api_slug
  record_id = id.is_a?(Hash) ? id["record_id"] : id
  "#{self.class.resource_path}/#{object_api_slug}/records/#{record_id}"
end

def retrieve(record_id: nil, object: nil, **opts)

Retrieve a specific record
def retrieve(record_id: nil, object: nil, **opts)
  validate_object_identifier!(object)
  # Extract simple ID if it's a nested hash
  simple_record_id = record_id.is_a?(Hash) ? record_id["record_id"] : record_id
  validate_id!(simple_record_id)
  response = execute_request(:GET, "#{resource_path}/#{object}/records/#{simple_record_id}", {}, opts)
  record_data = response["data"] || {}
  record_data["object_api_slug"] ||= object if record_data.is_a?(Hash)
  new(record_data, opts)
end

def save(**opts)

Save changes to the record
def save(**opts)
  raise InvalidRequestError, "Cannot update a record without an ID" unless persisted?
  raise InvalidRequestError, "Cannot save without object context" unless object_api_slug
  return self unless changed?
  params = {
    data: {
      values: prepare_values_for_update
    }
  }
  response = self.class.send(:execute_request, :PATCH, resource_path, params, opts)
  update_from(response[:data] || response)
  reset_changes!
  self
end

def search(query, object:, **opts)

This provides a basic search across common text fields
Note: The Attio API doesn't have a search endpoint, so we use filtering
Search records
def search(query, object:, **opts)
  # For now, just pass through to list with the query
  # Subclasses should override this to provide proper search filters
  list(object: object, **opts)
end

def to_h

Returns:
  • (Hash) - Record data as a hash
def to_h
  values = @attributes.except(:id, :created_at, :object_id, :object_api_slug)
  {
    id: id,
    object_id: attio_object_id,
    object_api_slug: object_api_slug,
    created_at: created_at&.iso8601,
    values: values
  }.compact
end

def update(record_id: nil, object: nil, data: nil, **opts)

Update a record
def update(record_id: nil, object: nil, data: nil, **opts)
  validate_object_identifier!(object)
  # Extract simple ID if it's a nested hash
  simple_record_id = record_id.is_a?(Hash) ? record_id["record_id"] : record_id
  validate_id!(simple_record_id)
  request_params = {
    data: {
      values: normalize_values(data[:values] || data)
    }
  }
  response = execute_request(:PUT, "#{resource_path}/#{object}/records/#{simple_record_id}", request_params, opts)
  record_data = response["data"] || {}
  record_data["object_api_slug"] ||= object if record_data.is_a?(Hash)
  new(record_data, opts)
end

def validate_object_identifier!(object)

def validate_object_identifier!(object)
  raise ArgumentError, "Object identifier is required" if object.nil? || object.to_s.empty?
end

def validate_values!(values)

def validate_values!(values)
  raise ArgumentError, "Values must be a Hash" unless values.is_a?(Hash)
end