class Attio::Attribute
Attributes define the schema for data stored on records
Represents a custom attribute on an Attio object
def self.resource_path
-
(String)
- The API path
def self.resource_path "attributes" end
def archive(**opts)
def archive(**opts) raise InvalidRequestError, "Cannot archive an attribute without an ID" unless persisted? response = self.class.send(:execute_request, :POST, "#{resource_path}/archive", {}, opts) update_from(response[:data] || response) self end
def archived?
def archived? @is_archived == true end
def create(params = {}, **opts)
def create(params = {}, **opts) object = params[:object] validate_object_identifier!(object) prepared_params = prepare_params_for_create(params) response = execute_request(:POST, "objects/#{object}/attributes", prepared_params, opts) new(response["data"] || response, opts) end
def for_object(object, params = {}, **)
def for_object(object, params = {}, **) list(params.merge(object: object), **) end
def has_default?
def has_default? is_default_value_enabled == true end
def initialize(attributes = {}, opts = {})
def initialize(attributes = {}, opts = {}) super normalized_attrs = normalize_attributes(attributes) @api_slug = normalized_attrs[:api_slug] @type = normalized_attrs[:type] @attio_object_id = normalized_attrs[:object_id] @object_api_slug = normalized_attrs[:object_api_slug] @parent_object_id = normalized_attrs[:parent_object_id] @created_by_actor = normalized_attrs[:created_by_actor] @is_archived = normalized_attrs[:is_archived] || false @archived_at = parse_timestamp(normalized_attrs[:archived_at]) @title = normalized_attrs[:title] end
def list(params = {}, **opts)
def list(params = {}, **opts) if params[:object] object = params.delete(:object) validate_object_identifier!(object) response = execute_request(:GET, "objects/#{object}/attributes", params, opts) APIResource::ListObject.new(response, self, params.merge(object: object), opts) else raise ArgumentError, "Attributes must be listed for a specific object. Use Attribute.for_object(object_slug) or pass object: parameter" end end
def prepare_options(options)
def prepare_options(options) return nil unless options case options when Array options.map do |opt| case opt when String {title: opt} when Hash opt else {title: opt.to_s} end end else options end end
def prepare_params_for_create(params)
def prepare_params_for_create(params) validate_type!(params[:type]) validate_type_config!(params) # Generate api_slug from name if not provided api_slug = params[:api_slug] || params[:name].downcase.gsub(/[^a-z0-9]+/, "_") { data: { title: params[:name] || params[:title], api_slug: api_slug, type: params[:type], description: params[:description], is_required: params[:is_required] || false, is_unique: params[:is_unique] || false, is_multiselect: params[:is_multiselect] || false, default_value: params[:default_value], config: params[:config] || {} }.compact } end
def prepare_params_for_update(params)
def prepare_params_for_update(params) # Only certain fields can be updated updateable_fields = %i[ name title description is_required is_unique default_value options ] update_params = params.slice(*updateable_fields) update_params[:options] = prepare_options(update_params[:options]) if update_params[:options] # Wrap in data for API { data: update_params } end
def required?
def required? is_required == true end
def resource_path
def resource_path raise InvalidRequestError, "Cannot generate path without an ID" unless persisted? attribute_id = Util::IdExtractor.extract_for_resource(id, :attribute) "#{self.class.resource_path}/#{attribute_id}" end
def retrieve(id, **opts)
def retrieve(id, **opts) # Extract simple ID if it's a nested hash attribute_id = Util::IdExtractor.extract_for_resource(id, :attribute) validate_id!(attribute_id) # For attributes, we need the object context - check if it's in the nested ID if id.is_a?(Hash) && id["object_id"] object_id = id["object_id"] response = execute_request(:GET, "objects/#{object_id}/attributes/#{attribute_id}", {}, opts) else # Fall back to regular attributes endpoint response = execute_request(:GET, "#{resource_path}/#{attribute_id}", {}, opts) end new(response["data"] || response, opts) end
def save(**)
def save(**) raise InvalidRequestError, "Cannot save an attribute without an ID" unless persisted? return self unless changed? # Pass the full ID (including object context) to update method self.class.update(id, changed_attributes, **) end
def to_h
-
(Hash)
- Attribute data as a hash
def to_h super.merge( api_slug: api_slug, name: name, description: description, type: type, is_required: is_required, is_unique: is_unique, is_default_value_enabled: is_default_value_enabled, default_value: default_value, options: options, object_id: attio_object_id, object_api_slug: object_api_slug, parent_object_id: parent_object_id, created_by_actor: created_by_actor, is_archived: is_archived, archived_at: archived_at&.iso8601 ).compact end
def unarchive(**opts)
def unarchive(**opts) raise InvalidRequestError, "Cannot unarchive an attribute without an ID" unless persisted? response = self.class.send(:execute_request, :POST, "#{resource_path}/unarchive", {}, opts) update_from(response[:data] || response) self end
def unique?
def unique? is_unique == true end
def update(id, params = {}, **opts)
def update(id, params = {}, **opts) # Extract simple ID if it's a nested hash attribute_id = Util::IdExtractor.extract_for_resource(id, :attribute) validate_id!(attribute_id) # For attributes, we need the object context if id.is_a?(Hash) && id["object_id"] object_id = id["object_id"] prepared_params = prepare_params_for_update(params) response = execute_request(:PATCH, "objects/#{object_id}/attributes/#{attribute_id}", prepared_params, opts) else # Fall back to regular attributes endpoint prepared_params = prepare_params_for_update(params) response = execute_request(:PATCH, "#{resource_path}/#{attribute_id}", prepared_params, opts) end new(response["data"] || response, 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_type!(type)
def validate_type!(type) raise ArgumentError, "Attribute type is required" if type.nil? || type.to_s.empty? unless TYPES.include?(type.to_s) raise ArgumentError, "Invalid attribute type: #{type}. Valid types: #{TYPES.join(", ")}" end end
def validate_type_config!(params)
def validate_type_config!(params) type = params[:type] config = TYPE_CONFIGS[type.to_s] return unless config # Check required options if config[:requires_options] options = params[:options] if options.nil? || (options.is_a?(Array) && options.empty?) raise ArgumentError, "Attribute type '#{type}' requires options" end end # Check required target object if config[:requires_target_object] target = params[:target_object] if target.nil? || target.to_s.empty? raise ArgumentError, "Attribute type '#{type}' requires target_object" end end # Validate unsupported features if params[:is_unique] && !config[:supports_unique] raise ArgumentError, "Attribute type '#{type}' does not support unique constraint" end if params[:is_required] && !config[:supports_required] raise ArgumentError, "Attribute type '#{type}' does not support required constraint" end if params[:is_default_value_enabled] && !config[:supports_default] raise ArgumentError, "Attribute type '#{type}' does not support default values" end end