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
-
(String)
- The API path
def self.resource_path "objects" end
def add_to_list(list_id, **)
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)
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)
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
-
(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)
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(**)
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)
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)
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)
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
-
(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)
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