attio 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -9,6 +9,20 @@ module Attio
9
9
  #
10
10
  # @example Listing attributes
11
11
  # client.attributes.list(object: "people")
12
+ #
13
+ # @example Creating a custom attribute
14
+ # client.attributes.create(
15
+ # object: "deals",
16
+ # data: {
17
+ # title: "Deal Stage",
18
+ # api_slug: "deal_stage",
19
+ # type: "select",
20
+ # options: [
21
+ # { title: "Lead", value: "lead" },
22
+ # { title: "Qualified", value: "qualified" }
23
+ # ]
24
+ # }
25
+ # )
12
26
  class Attributes < Base
13
27
  def list(object:, **params)
14
28
  validate_object!(object)
@@ -21,6 +35,228 @@ module Attio
21
35
  request(:get, "objects/#{object}/attributes/#{id_or_slug}")
22
36
  end
23
37
 
38
+ # Create a custom attribute for an object
39
+ #
40
+ # @param object [String] The object type or slug
41
+ # @param data [Hash] The attribute configuration
42
+ # @option data [String] :title The display title of the attribute
43
+ # @option data [String] :api_slug The API slug for the attribute
44
+ # @option data [String] :type The attribute type (text, number, select, date, etc.)
45
+ # @option data [String] :description Optional description
46
+ # @option data [Boolean] :is_required Whether the attribute is required
47
+ # @option data [Boolean] :is_unique Whether the attribute must be unique
48
+ # @option data [Boolean] :is_multiselect For select types, whether multiple values are allowed
49
+ # @option data [Array<Hash>] :options For select types, the available options
50
+ # @return [Hash] The created attribute
51
+ # @example Create a select attribute
52
+ # client.attributes.create(
53
+ # object: "trips",
54
+ # data: {
55
+ # title: "Status",
56
+ # api_slug: "status",
57
+ # type: "select",
58
+ # options: [
59
+ # { title: "Pending", value: "pending" },
60
+ # { title: "Active", value: "active" }
61
+ # ]
62
+ # }
63
+ # )
64
+ def create(object:, data:)
65
+ validate_object!(object)
66
+ validate_required_hash!(data, "Attribute data")
67
+
68
+ # Wrap data in the expected format
69
+ payload = { data: data }
70
+
71
+ request(:post, "objects/#{object}/attributes", payload)
72
+ end
73
+
74
+ # Update an existing attribute
75
+ #
76
+ # @param object [String] The object type or slug
77
+ # @param id_or_slug [String] The attribute ID or API slug
78
+ # @param data [Hash] The attribute configuration updates
79
+ # @option data [String] :title The display title of the attribute
80
+ # @option data [String] :api_slug The API slug for the attribute
81
+ # @option data [String] :description Optional description
82
+ # @option data [Boolean] :is_required Whether the attribute is required
83
+ # @option data [Boolean] :is_unique Whether the attribute must be unique
84
+ # @option data [Boolean] :is_multiselect For select types, whether multiple values are allowed
85
+ # @return [Hash] The updated attribute
86
+ # @example Update an attribute's title
87
+ # client.attributes.update(
88
+ # object: "contacts",
89
+ # id_or_slug: "status",
90
+ # data: {
91
+ # title: "Contact Status",
92
+ # description: "The current status of the contact"
93
+ # }
94
+ # )
95
+ def update(object:, id_or_slug:, data:)
96
+ validate_object!(object)
97
+ validate_id_or_slug!(id_or_slug)
98
+ validate_required_hash!(data, "Attribute data")
99
+
100
+ # Wrap data in the expected format
101
+ payload = { data: data }
102
+
103
+ request(:patch, "objects/#{object}/attributes/#{id_or_slug}", payload)
104
+ end
105
+
106
+ # List options for a select attribute
107
+ #
108
+ # @param object [String] The object type or slug
109
+ # @param id_or_slug [String] The attribute ID or API slug
110
+ # @param params [Hash] Additional query parameters
111
+ # @option params [Integer] :limit Number of results to return
112
+ # @option params [Integer] :offset Number of results to skip
113
+ # @return [Hash] The list of attribute options
114
+ # @example List options for a select attribute
115
+ # client.attributes.list_options(
116
+ # object: "deals",
117
+ # id_or_slug: "deal_stage"
118
+ # )
119
+ def list_options(object:, id_or_slug:, **params)
120
+ validate_object!(object)
121
+ validate_id_or_slug!(id_or_slug)
122
+ request(:get, "objects/#{object}/attributes/#{id_or_slug}/options", params)
123
+ end
124
+
125
+ # Create a new option for a select attribute
126
+ #
127
+ # @param object [String] The object type or slug
128
+ # @param id_or_slug [String] The attribute ID or API slug
129
+ # @param data [Hash] The option configuration
130
+ # @option data [String] :title The display title of the option
131
+ # @option data [String] :value The value of the option
132
+ # @option data [String] :color Optional color for the option
133
+ # @option data [Integer] :order Optional order for the option
134
+ # @return [Hash] The created option
135
+ # @example Create a new option
136
+ # client.attributes.create_option(
137
+ # object: "deals",
138
+ # id_or_slug: "deal_stage",
139
+ # data: {
140
+ # title: "Negotiation",
141
+ # value: "negotiation",
142
+ # color: "blue"
143
+ # }
144
+ # )
145
+ def create_option(object:, id_or_slug:, data:)
146
+ validate_object!(object)
147
+ validate_id_or_slug!(id_or_slug)
148
+ validate_required_hash!(data, "Option data")
149
+
150
+ request(:post, "objects/#{object}/attributes/#{id_or_slug}/options", data)
151
+ end
152
+
153
+ # Update an option for a select attribute
154
+ #
155
+ # @param object [String] The object type or slug
156
+ # @param id_or_slug [String] The attribute ID or API slug
157
+ # @param option [String] The option ID or value
158
+ # @param data [Hash] The option configuration updates
159
+ # @option data [String] :title The display title of the option
160
+ # @option data [String] :value The value of the option
161
+ # @option data [String] :color Optional color for the option
162
+ # @option data [Integer] :order Optional order for the option
163
+ # @return [Hash] The updated option
164
+ # @example Update an option's title
165
+ # client.attributes.update_option(
166
+ # object: "deals",
167
+ # id_or_slug: "deal_stage",
168
+ # option: "negotiation",
169
+ # data: {
170
+ # title: "In Negotiation",
171
+ # color: "orange"
172
+ # }
173
+ # )
174
+ def update_option(object:, id_or_slug:, option:, data:)
175
+ validate_object!(object)
176
+ validate_id_or_slug!(id_or_slug)
177
+ validate_option_id!(option)
178
+ validate_required_hash!(data, "Option data")
179
+
180
+ request(:patch, "objects/#{object}/attributes/#{id_or_slug}/options/#{option}", data)
181
+ end
182
+
183
+ # List statuses for a status attribute
184
+ #
185
+ # @param object [String] The object type or slug
186
+ # @param id_or_slug [String] The attribute ID or API slug
187
+ # @param params [Hash] Additional query parameters
188
+ # @option params [Integer] :limit Number of results to return
189
+ # @option params [Integer] :offset Number of results to skip
190
+ # @return [Hash] The list of attribute statuses
191
+ # @example List statuses for a status attribute
192
+ # client.attributes.list_statuses(
193
+ # object: "deals",
194
+ # id_or_slug: "deal_status"
195
+ # )
196
+ def list_statuses(object:, id_or_slug:, **params)
197
+ validate_object!(object)
198
+ validate_id_or_slug!(id_or_slug)
199
+ request(:get, "objects/#{object}/attributes/#{id_or_slug}/statuses", params)
200
+ end
201
+
202
+ # Create a new status for a status attribute
203
+ #
204
+ # @param object [String] The object type or slug
205
+ # @param id_or_slug [String] The attribute ID or API slug
206
+ # @param data [Hash] The status configuration
207
+ # @option data [String] :title The display title of the status
208
+ # @option data [String] :value The value of the status
209
+ # @option data [String] :color Optional color for the status
210
+ # @option data [Integer] :order Optional order for the status
211
+ # @return [Hash] The created status
212
+ # @example Create a new status
213
+ # client.attributes.create_status(
214
+ # object: "deals",
215
+ # id_or_slug: "deal_status",
216
+ # data: {
217
+ # title: "Under Review",
218
+ # value: "under_review",
219
+ # color: "yellow"
220
+ # }
221
+ # )
222
+ def create_status(object:, id_or_slug:, data:)
223
+ validate_object!(object)
224
+ validate_id_or_slug!(id_or_slug)
225
+ validate_required_hash!(data, "Status data")
226
+
227
+ request(:post, "objects/#{object}/attributes/#{id_or_slug}/statuses", data)
228
+ end
229
+
230
+ # Update a status for a status attribute
231
+ #
232
+ # @param object [String] The object type or slug
233
+ # @param id_or_slug [String] The attribute ID or API slug
234
+ # @param status [String] The status ID or value
235
+ # @param data [Hash] The status configuration updates
236
+ # @option data [String] :title The display title of the status
237
+ # @option data [String] :value The value of the status
238
+ # @option data [String] :color Optional color for the status
239
+ # @option data [Integer] :order Optional order for the status
240
+ # @return [Hash] The updated status
241
+ # @example Update a status's title
242
+ # client.attributes.update_status(
243
+ # object: "deals",
244
+ # id_or_slug: "deal_status",
245
+ # status: "under_review",
246
+ # data: {
247
+ # title: "Pending Review",
248
+ # color: "orange"
249
+ # }
250
+ # )
251
+ def update_status(object:, id_or_slug:, status:, data:)
252
+ validate_object!(object)
253
+ validate_id_or_slug!(id_or_slug)
254
+ validate_status_id!(status)
255
+ validate_required_hash!(data, "Status data")
256
+
257
+ request(:patch, "objects/#{object}/attributes/#{id_or_slug}/statuses/#{status}", data)
258
+ end
259
+
24
260
  private def validate_object!(object)
25
261
  raise ArgumentError, "Object type is required" if object.nil? || object.to_s.strip.empty?
26
262
  end
@@ -28,6 +264,14 @@ module Attio
28
264
  private def validate_id_or_slug!(id_or_slug)
29
265
  raise ArgumentError, "Attribute ID or slug is required" if id_or_slug.nil? || id_or_slug.to_s.strip.empty?
30
266
  end
267
+
268
+ private def validate_option_id!(option)
269
+ raise ArgumentError, "Option ID is required" if option.nil? || option.to_s.strip.empty?
270
+ end
271
+
272
+ private def validate_status_id!(status)
273
+ raise ArgumentError, "Status ID is required" if status.nil? || status.to_s.strip.empty?
274
+ end
31
275
  end
32
276
  end
33
277
  end
@@ -44,10 +44,205 @@ module Attio
44
44
  request(:delete, "lists/#{list_id}/entries/#{entry_id}")
45
45
  end
46
46
 
47
+ # Create a new list.
48
+ #
49
+ # Creates a new list with the specified configuration. The list can be
50
+ # associated with a parent object type and configured with various options.
51
+ #
52
+ # @param data [Hash] The list configuration data
53
+ # @option data [String] :name The name of the list (required)
54
+ # @option data [String] :parent_object The parent object type for the list
55
+ # @option data [Boolean] :is_public Whether the list is publicly accessible
56
+ # @option data [String] :description Optional description for the list
57
+ #
58
+ # @return [Hash] The created list data
59
+ # @raise [ArgumentError] if data is invalid
60
+ #
61
+ # @example Create a simple list
62
+ # list = client.lists.create(
63
+ # data: {
64
+ # name: "VIP Customers",
65
+ # parent_object: "people",
66
+ # is_public: true
67
+ # }
68
+ # )
69
+ def create(data:)
70
+ validate_list_data!(data)
71
+ request(:post, "lists", { data: data })
72
+ end
73
+
74
+ # Update an existing list.
75
+ #
76
+ # Updates the configuration of an existing list. You can modify the name,
77
+ # description, visibility, and other list properties.
78
+ #
79
+ # @param id_or_slug [String] The list ID or slug
80
+ # @param data [Hash] The list configuration updates
81
+ # @option data [String] :name New name for the list
82
+ # @option data [Boolean] :is_public New visibility setting
83
+ # @option data [String] :description New description for the list
84
+ #
85
+ # @return [Hash] The updated list data
86
+ # @raise [ArgumentError] if id_or_slug or data is invalid
87
+ #
88
+ # @example Update list name and visibility
89
+ # list = client.lists.update(
90
+ # id_or_slug: "list_123",
91
+ # data: {
92
+ # name: "Premium Customers",
93
+ # is_public: false
94
+ # }
95
+ # )
96
+ def update(id_or_slug:, data:)
97
+ validate_id!(id_or_slug, "List")
98
+ validate_list_data!(data)
99
+ request(:patch, "lists/#{id_or_slug}", { data: data })
100
+ end
101
+
102
+ # Query list entries with advanced filtering and sorting.
103
+ #
104
+ # Provides advanced querying capabilities for list entries, supporting
105
+ # complex filters, sorting, and pagination. This is more powerful than
106
+ # the basic entries method as it supports structured queries.
107
+ #
108
+ # @param id_or_slug [String] The list ID or slug
109
+ # @param filter [Hash, String, nil] Optional filter criteria for querying entries
110
+ # @param sort [Hash, Array, String, nil] Optional sorting configuration
111
+ # @param limit [Integer, nil] Maximum number of entries to return
112
+ # @param offset [Integer, nil] Number of entries to skip for pagination
113
+ #
114
+ # @return [Hash] Query results with entries and pagination info
115
+ # @raise [ArgumentError] if id_or_slug is invalid
116
+ #
117
+ # @example Query with filters
118
+ # entries = client.lists.query_entries(
119
+ # id_or_slug: "vip_customers",
120
+ # filter: { created_at: { gte: "2023-01-01" } },
121
+ # sort: { created_at: "desc" },
122
+ # limit: 50
123
+ # )
124
+ #
125
+ # @example Query all entries without filters
126
+ # entries = client.lists.query_entries(id_or_slug: "list_123")
127
+ def query_entries(id_or_slug:, filter: nil, sort: nil, limit: nil, offset: nil)
128
+ validate_id!(id_or_slug, "List")
129
+
130
+ query_params = build_query_params({
131
+ filter: filter,
132
+ sort: sort,
133
+ limit: limit,
134
+ offset: offset,
135
+ })
136
+
137
+ request(:post, "lists/#{id_or_slug}/entries/query", query_params)
138
+ end
139
+
140
+ # Assert (upsert) a list entry.
141
+ #
142
+ # Creates or updates a list entry based on a matching attribute. This is an
143
+ # upsert operation that will create a new entry if no match is found, or update
144
+ # an existing entry if a match is found based on the specified matching attribute.
145
+ #
146
+ # Required scopes: list_entry:read-write, list_configuration:read
147
+ #
148
+ # @param id_or_slug [String] The list ID or slug
149
+ # @param matching_attribute [String] The attribute to match against for upsert
150
+ # @param data [Hash] The entry data to create or update
151
+ # @option data [String] :record_id The ID of the record to add to the list
152
+ # @option data [Hash] :values Optional field values for the entry
153
+ # @option data [String] :notes Optional notes for the entry
154
+ #
155
+ # @return [Hash] The created or updated entry data
156
+ # @raise [ArgumentError] if parameters are invalid
157
+ #
158
+ # @example Assert entry with record ID
159
+ # entry = client.lists.assert_entry(
160
+ # id_or_slug: "vip_customers",
161
+ # matching_attribute: "record_id",
162
+ # data: {
163
+ # record_id: "rec_123",
164
+ # values: { priority: "high" },
165
+ # notes: "Premium customer"
166
+ # }
167
+ # )
168
+ #
169
+ # @example Assert entry with custom matching
170
+ # entry = client.lists.assert_entry(
171
+ # id_or_slug: "lead_list",
172
+ # matching_attribute: "email",
173
+ # data: {
174
+ # email: "john@example.com",
175
+ # values: { status: "qualified" }
176
+ # }
177
+ # )
178
+ def assert_entry(id_or_slug:, matching_attribute:, data:)
179
+ validate_id!(id_or_slug, "List")
180
+ validate_required_string!(matching_attribute, "Matching attribute")
181
+ validate_list_entry_data!(data)
182
+
183
+ request_body = {
184
+ data: data,
185
+ matching_attribute: matching_attribute,
186
+ }
187
+
188
+ request(:put, "lists/#{id_or_slug}/entries", request_body)
189
+ end
190
+
191
+ # Update an existing list entry.
192
+ #
193
+ # Updates the data for an existing list entry. This method requires the
194
+ # entry ID and will update only the provided fields, leaving other fields
195
+ # unchanged.
196
+ #
197
+ # @param id_or_slug [String] The list ID or slug
198
+ # @param entry_id [String] The entry ID to update
199
+ # @param data [Hash] The entry data to update
200
+ # @option data [Hash] :values Field values to update
201
+ # @option data [String] :notes Updated notes for the entry
202
+ #
203
+ # @return [Hash] The updated entry data
204
+ # @raise [ArgumentError] if parameters are invalid
205
+ #
206
+ # @example Update entry values
207
+ # entry = client.lists.update_entry(
208
+ # id_or_slug: "vip_customers",
209
+ # entry_id: "ent_456",
210
+ # data: {
211
+ # values: { priority: "medium", last_contacted: "2024-01-15" },
212
+ # notes: "Updated contact information"
213
+ # }
214
+ # )
215
+ #
216
+ # @example Update only notes
217
+ # entry = client.lists.update_entry(
218
+ # id_or_slug: "lead_list",
219
+ # entry_id: "ent_789",
220
+ # data: { notes: "Follow up scheduled" }
221
+ # )
222
+ def update_entry(id_or_slug:, entry_id:, data:)
223
+ validate_id!(id_or_slug, "List")
224
+ validate_id!(entry_id, "Entry")
225
+ validate_list_entry_data!(data)
226
+
227
+ request_body = { data: data }
228
+
229
+ request(:patch, "lists/#{id_or_slug}/entries/#{entry_id}", request_body)
230
+ end
231
+
47
232
  private def validate_list_entry_data!(data)
48
233
  raise ArgumentError, "Data is required" if data.nil?
49
234
  raise ArgumentError, "Data must be a hash" unless data.is_a?(Hash)
50
235
  end
236
+
237
+ # Validates that the list data parameter is present and valid.
238
+ #
239
+ # @param data [Hash, nil] The data to validate
240
+ # @raise [ArgumentError] if data is nil or not a hash
241
+ # @api private
242
+ private def validate_list_data!(data)
243
+ raise ArgumentError, "Data is required" if data.nil?
244
+ raise ArgumentError, "Data must be a hash" unless data.is_a?(Hash)
245
+ end
51
246
  end
52
247
  end
53
248
  end
@@ -0,0 +1,133 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Attio
4
+ module Resources
5
+ # Meta resource for getting information about the API token and workspace
6
+ #
7
+ # The Meta resource provides a single endpoint (/v2/self) that returns
8
+ # information about the current access token, workspace, and permissions.
9
+ #
10
+ # @example Get token and workspace information
11
+ # meta_info = client.meta.identify
12
+ # puts "Workspace: #{meta_info['data']['workspace_name']}"
13
+ # puts "Token active: #{meta_info['data']['active']}"
14
+ #
15
+ # @example Check if token is active
16
+ # if client.meta.active?
17
+ # puts "Token is valid and active"
18
+ # end
19
+ #
20
+ # @example Get workspace details
21
+ # workspace = client.meta.workspace
22
+ # puts "Working in: #{workspace['name']} (#{workspace['id']})"
23
+ #
24
+ # @example Check permissions
25
+ # if client.meta.permission?("record_permission:read-write")
26
+ # # Can read and write records
27
+ # end
28
+ class Meta < Base
29
+ # Get information about the current access token and workspace
30
+ #
31
+ # This is the primary method that calls the /v2/self endpoint.
32
+ # Returns full token metadata including workspace info and permissions.
33
+ #
34
+ # @return [Hash] Token and workspace information
35
+ # @example
36
+ # info = client.meta.identify
37
+ # # => { "data" => { "active" => true, "workspace_name" => "My Workspace", ... } }
38
+ def identify
39
+ request(:get, "self")
40
+ end
41
+
42
+ # Alias methods for convenience and clarity
43
+ alias self identify
44
+ alias get identify
45
+
46
+ # Check if the current token is active
47
+ #
48
+ # @return [Boolean] true if token is active, false otherwise
49
+ # @example
50
+ # if client.meta.active?
51
+ # # Proceed with API calls
52
+ # else
53
+ # # Token is inactive or invalid
54
+ # end
55
+ def active?
56
+ response = identify
57
+ response.dig("data", "active") || false
58
+ end
59
+
60
+ # Get the workspace information
61
+ #
62
+ # Returns workspace details if token is active, nil otherwise.
63
+ #
64
+ # @return [Hash, nil] Workspace details or nil if token inactive
65
+ # @example
66
+ # workspace = client.meta.workspace
67
+ # # => { "id" => "uuid", "name" => "My Workspace", "slug" => "my-workspace", "logo_url" => nil }
68
+ def workspace
69
+ response = identify
70
+ return nil unless response.dig("data", "active")
71
+
72
+ {
73
+ "id" => response.dig("data", "workspace_id"),
74
+ "name" => response.dig("data", "workspace_name"),
75
+ "slug" => response.dig("data", "workspace_slug"),
76
+ "logo_url" => response.dig("data", "workspace_logo_url"),
77
+ }
78
+ end
79
+
80
+ # Get the token's permissions/scopes
81
+ #
82
+ # Parses the space-separated scope string into an array.
83
+ #
84
+ # @return [Array<String>] List of permission scopes
85
+ # @example
86
+ # permissions = client.meta.permissions
87
+ # # => ["comment:read-write", "list_configuration:read", "note:read-write"]
88
+ def permissions
89
+ response = identify
90
+ scope = response.dig("data", "scope") || ""
91
+ scope.split
92
+ end
93
+
94
+ # Check if token has a specific permission
95
+ #
96
+ # @param permission [String] The permission to check (e.g., "comment:read-write")
97
+ # @return [Boolean] true if permission is granted
98
+ # @example
99
+ # if client.meta.permission?("record_permission:read-write")
100
+ # # Can manage records
101
+ # end
102
+ def permission?(permission)
103
+ permissions.include?(permission)
104
+ end
105
+
106
+ # Alias for backward compatibility
107
+ alias has_permission? permission?
108
+
109
+ # Get token expiration and metadata
110
+ #
111
+ # Returns detailed token information including expiration,
112
+ # issue time, client ID, and who authorized it.
113
+ #
114
+ # @return [Hash] Token metadata
115
+ # @example
116
+ # info = client.meta.token_info
117
+ # # => { "active" => true, "type" => "Bearer", "expires_at" => nil, ... }
118
+ def token_info
119
+ response = identify
120
+ return { "active" => false } unless response.dig("data", "active")
121
+
122
+ {
123
+ "active" => true,
124
+ "type" => response.dig("data", "token_type"),
125
+ "expires_at" => response.dig("data", "exp"),
126
+ "issued_at" => response.dig("data", "iat"),
127
+ "client_id" => response.dig("data", "client_id"),
128
+ "authorized_by" => response.dig("data", "authorized_by_workspace_member_id"),
129
+ }
130
+ end
131
+ end
132
+ end
133
+ end