attio-ruby 0.1.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.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +164 -0
- data/.simplecov +17 -0
- data/.yardopts +9 -0
- data/CHANGELOG.md +27 -0
- data/CONTRIBUTING.md +333 -0
- data/INTEGRATION_TEST_STATUS.md +149 -0
- data/LICENSE +21 -0
- data/README.md +638 -0
- data/Rakefile +8 -0
- data/attio-ruby.gemspec +61 -0
- data/docs/CODECOV_SETUP.md +34 -0
- data/examples/basic_usage.rb +149 -0
- data/examples/oauth_flow.rb +843 -0
- data/examples/oauth_flow_README.md +84 -0
- data/examples/typed_records_example.rb +167 -0
- data/examples/webhook_server.rb +463 -0
- data/lib/attio/api_resource.rb +539 -0
- data/lib/attio/builders/name_builder.rb +181 -0
- data/lib/attio/client.rb +160 -0
- data/lib/attio/errors.rb +126 -0
- data/lib/attio/internal/record.rb +359 -0
- data/lib/attio/oauth/client.rb +219 -0
- data/lib/attio/oauth/scope_validator.rb +162 -0
- data/lib/attio/oauth/token.rb +158 -0
- data/lib/attio/resources/attribute.rb +332 -0
- data/lib/attio/resources/comment.rb +114 -0
- data/lib/attio/resources/company.rb +224 -0
- data/lib/attio/resources/entry.rb +208 -0
- data/lib/attio/resources/list.rb +196 -0
- data/lib/attio/resources/meta.rb +113 -0
- data/lib/attio/resources/note.rb +213 -0
- data/lib/attio/resources/object.rb +66 -0
- data/lib/attio/resources/person.rb +294 -0
- data/lib/attio/resources/task.rb +147 -0
- data/lib/attio/resources/thread.rb +99 -0
- data/lib/attio/resources/typed_record.rb +98 -0
- data/lib/attio/resources/webhook.rb +224 -0
- data/lib/attio/resources/workspace_member.rb +136 -0
- data/lib/attio/util/configuration.rb +166 -0
- data/lib/attio/util/id_extractor.rb +115 -0
- data/lib/attio/util/webhook_signature.rb +175 -0
- data/lib/attio/version.rb +6 -0
- data/lib/attio/webhook/event.rb +114 -0
- data/lib/attio/webhook/signature_verifier.rb +73 -0
- data/lib/attio.rb +123 -0
- metadata +402 -0
@@ -0,0 +1,208 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "../api_resource"
|
4
|
+
|
5
|
+
module Attio
|
6
|
+
# Represents an entry in an Attio list
|
7
|
+
# Entries link records to lists with custom attribute values
|
8
|
+
class Entry < APIResource
|
9
|
+
attr_reader :parent_record_id, :parent_object, :list_id
|
10
|
+
attr_accessor :entry_values
|
11
|
+
|
12
|
+
def initialize(attributes = {}, opts = {})
|
13
|
+
super
|
14
|
+
|
15
|
+
normalized_attrs = normalize_attributes(attributes)
|
16
|
+
|
17
|
+
# Extract specific entry attributes
|
18
|
+
@parent_record_id = normalized_attrs[:parent_record_id]
|
19
|
+
@parent_object = normalized_attrs[:parent_object]
|
20
|
+
@entry_values = normalized_attrs[:entry_values] || {}
|
21
|
+
|
22
|
+
# Extract list_id from nested ID structure
|
23
|
+
if normalized_attrs[:id].is_a?(Hash)
|
24
|
+
@list_id = normalized_attrs[:id][:list_id]
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
class << self
|
29
|
+
# API endpoint path for entries (nested under lists)
|
30
|
+
# @return [String] The API path
|
31
|
+
def resource_path
|
32
|
+
"lists"
|
33
|
+
end
|
34
|
+
|
35
|
+
# List entries for a list
|
36
|
+
def list(list: nil, **params)
|
37
|
+
validate_list_identifier!(list)
|
38
|
+
|
39
|
+
response = execute_request(:POST, "#{resource_path}/#{list}/entries/query", params, {})
|
40
|
+
APIResource::ListObject.new(response, self, params.merge(list: list), params)
|
41
|
+
end
|
42
|
+
alias_method :all, :list
|
43
|
+
|
44
|
+
# Create a new entry
|
45
|
+
def create(list: nil, parent_record_id: nil, parent_object: nil, entry_values: nil, **opts)
|
46
|
+
validate_list_identifier!(list)
|
47
|
+
validate_parent_params!(parent_record_id, parent_object)
|
48
|
+
|
49
|
+
request_params = {
|
50
|
+
data: {
|
51
|
+
parent_record_id: parent_record_id,
|
52
|
+
parent_object: parent_object,
|
53
|
+
entry_values: entry_values || {}
|
54
|
+
}
|
55
|
+
}
|
56
|
+
|
57
|
+
response = execute_request(:POST, "#{resource_path}/#{list}/entries", request_params, opts)
|
58
|
+
new(response["data"] || response, opts)
|
59
|
+
end
|
60
|
+
|
61
|
+
# Retrieve a specific entry
|
62
|
+
def retrieve(list: nil, entry_id: nil, **opts)
|
63
|
+
validate_list_identifier!(list)
|
64
|
+
validate_entry_id!(entry_id)
|
65
|
+
|
66
|
+
response = execute_request(:GET, "#{resource_path}/#{list}/entries/#{entry_id}", {}, opts)
|
67
|
+
new(response["data"] || response, opts)
|
68
|
+
end
|
69
|
+
alias_method :get, :retrieve
|
70
|
+
alias_method :find, :retrieve
|
71
|
+
|
72
|
+
# Update an entry
|
73
|
+
def update(list: nil, entry_id: nil, entry_values: nil, mode: nil, **opts)
|
74
|
+
validate_list_identifier!(list)
|
75
|
+
validate_entry_id!(entry_id)
|
76
|
+
|
77
|
+
request_params = {
|
78
|
+
data: {
|
79
|
+
entry_values: entry_values || {}
|
80
|
+
}
|
81
|
+
}
|
82
|
+
|
83
|
+
# Add mode parameter for append operations
|
84
|
+
if mode == "append"
|
85
|
+
request_params[:mode] = "append"
|
86
|
+
end
|
87
|
+
|
88
|
+
response = execute_request(:PATCH, "#{resource_path}/#{list}/entries/#{entry_id}", request_params, opts)
|
89
|
+
new(response["data"] || response, opts)
|
90
|
+
end
|
91
|
+
|
92
|
+
# Delete an entry
|
93
|
+
def delete(list: nil, entry_id: nil, **opts)
|
94
|
+
validate_list_identifier!(list)
|
95
|
+
validate_entry_id!(entry_id)
|
96
|
+
|
97
|
+
execute_request(:DELETE, "#{resource_path}/#{list}/entries/#{entry_id}", {}, opts)
|
98
|
+
true
|
99
|
+
end
|
100
|
+
alias_method :destroy, :delete
|
101
|
+
|
102
|
+
# Assert an entry by parent record
|
103
|
+
def assert_by_parent(list: nil, parent_record_id: nil, parent_object: nil, entry_values: nil, **opts)
|
104
|
+
validate_list_identifier!(list)
|
105
|
+
validate_parent_params!(parent_record_id, parent_object)
|
106
|
+
|
107
|
+
request_params = {
|
108
|
+
data: {
|
109
|
+
parent_record_id: parent_record_id,
|
110
|
+
parent_object: parent_object,
|
111
|
+
entry_values: entry_values || {}
|
112
|
+
}
|
113
|
+
}
|
114
|
+
|
115
|
+
response = execute_request(:PUT, "#{resource_path}/#{list}/entries", request_params, opts)
|
116
|
+
new(response["data"] || response, opts)
|
117
|
+
end
|
118
|
+
|
119
|
+
# List attribute values for an entry
|
120
|
+
def list_attribute_values(list: nil, entry_id: nil, attribute_id: nil, **opts)
|
121
|
+
validate_list_identifier!(list)
|
122
|
+
validate_entry_id!(entry_id)
|
123
|
+
raise ArgumentError, "Attribute ID is required" if attribute_id.nil? || attribute_id.to_s.empty?
|
124
|
+
|
125
|
+
response = execute_request(:GET, "#{resource_path}/#{list}/entries/#{entry_id}/attributes/#{attribute_id}/values", {}, opts)
|
126
|
+
response["data"] || []
|
127
|
+
end
|
128
|
+
|
129
|
+
private
|
130
|
+
|
131
|
+
def validate_list_identifier!(list)
|
132
|
+
raise ArgumentError, "List identifier is required" if list.nil? || list.to_s.empty?
|
133
|
+
end
|
134
|
+
|
135
|
+
def validate_entry_id!(entry_id)
|
136
|
+
raise ArgumentError, "Entry ID is required" if entry_id.nil? || entry_id.to_s.empty?
|
137
|
+
end
|
138
|
+
|
139
|
+
def validate_parent_params!(parent_record_id, parent_object)
|
140
|
+
if parent_record_id.nil? || parent_object.nil?
|
141
|
+
raise ArgumentError, "parent_record_id and parent_object are required"
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
# Instance methods
|
147
|
+
|
148
|
+
def save(**opts)
|
149
|
+
raise InvalidRequestError, "Cannot save an entry without an ID" unless persisted?
|
150
|
+
raise InvalidRequestError, "Cannot save without list context" unless list_id
|
151
|
+
|
152
|
+
# For Entry, we always save the full entry_values
|
153
|
+
params = {
|
154
|
+
data: {
|
155
|
+
entry_values: entry_values
|
156
|
+
}
|
157
|
+
}
|
158
|
+
|
159
|
+
response = self.class.send(:execute_request, :PATCH, resource_path, params, opts)
|
160
|
+
update_from(response[:data] || response)
|
161
|
+
reset_changes!
|
162
|
+
self
|
163
|
+
end
|
164
|
+
|
165
|
+
def destroy(**opts)
|
166
|
+
raise InvalidRequestError, "Cannot destroy an entry without an ID" unless persisted?
|
167
|
+
raise InvalidRequestError, "Cannot destroy without list context" unless list_id
|
168
|
+
|
169
|
+
entry_id = extract_entry_id
|
170
|
+
self.class.send(:execute_request, :DELETE, "lists/#{list_id}/entries/#{entry_id}", {}, opts)
|
171
|
+
@attributes.clear
|
172
|
+
@changed_attributes.clear
|
173
|
+
@id = nil
|
174
|
+
true
|
175
|
+
end
|
176
|
+
|
177
|
+
def resource_path
|
178
|
+
raise InvalidRequestError, "Cannot generate path without list context" unless list_id
|
179
|
+
entry_id = extract_entry_id
|
180
|
+
"lists/#{list_id}/entries/#{entry_id}"
|
181
|
+
end
|
182
|
+
|
183
|
+
private
|
184
|
+
|
185
|
+
def extract_entry_id
|
186
|
+
case id
|
187
|
+
when Hash
|
188
|
+
id[:entry_id] || id["entry_id"]
|
189
|
+
else
|
190
|
+
id
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
def to_h
|
195
|
+
{
|
196
|
+
id: id,
|
197
|
+
parent_record_id: parent_record_id,
|
198
|
+
parent_object: parent_object,
|
199
|
+
created_at: created_at&.iso8601,
|
200
|
+
entry_values: entry_values
|
201
|
+
}.compact
|
202
|
+
end
|
203
|
+
|
204
|
+
def inspect
|
205
|
+
"#<#{self.class.name}:#{object_id} id=#{id.inspect} parent=#{parent_object}##{parent_record_id} values=#{entry_values.inspect}>"
|
206
|
+
end
|
207
|
+
end
|
208
|
+
end
|
@@ -0,0 +1,196 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "ostruct"
|
4
|
+
require_relative "../api_resource"
|
5
|
+
|
6
|
+
module Attio
|
7
|
+
# Represents a list in Attio for organizing records
|
8
|
+
class List < APIResource
|
9
|
+
api_operations :list, :retrieve, :create, :update
|
10
|
+
|
11
|
+
# API endpoint path for lists
|
12
|
+
# @return [String] The API path
|
13
|
+
def self.resource_path
|
14
|
+
"lists"
|
15
|
+
end
|
16
|
+
|
17
|
+
# Define known attributes with proper accessors
|
18
|
+
attr_attio :name, :workspace_access
|
19
|
+
|
20
|
+
# Read-only attributes
|
21
|
+
attr_reader :api_slug, :attio_object_id, :object_api_slug,
|
22
|
+
:created_by_actor, :workspace_id, :parent_object, :filters
|
23
|
+
|
24
|
+
# Get the parent object as a string
|
25
|
+
def object
|
26
|
+
# parent_object is returned as an array from the API
|
27
|
+
return nil unless @parent_object
|
28
|
+
@parent_object.is_a?(Array) ? @parent_object.first : @parent_object
|
29
|
+
end
|
30
|
+
|
31
|
+
def initialize(attributes = {}, opts = {})
|
32
|
+
super
|
33
|
+
|
34
|
+
# Now we can safely use symbol keys only since parent normalized them
|
35
|
+
normalized_attrs = normalize_attributes(attributes)
|
36
|
+
@api_slug = normalized_attrs[:api_slug]
|
37
|
+
@name = normalized_attrs[:name]
|
38
|
+
@attio_object_id = normalized_attrs[:object_id]
|
39
|
+
@object_api_slug = normalized_attrs[:object_api_slug]
|
40
|
+
@created_by_actor = normalized_attrs[:created_by_actor]
|
41
|
+
@workspace_id = normalized_attrs[:workspace_id]
|
42
|
+
@workspace_access = normalized_attrs[:workspace_access]
|
43
|
+
@parent_object = normalized_attrs[:parent_object] || normalized_attrs[:object]
|
44
|
+
@filters = normalized_attrs[:filters]
|
45
|
+
end
|
46
|
+
|
47
|
+
def resource_path
|
48
|
+
raise InvalidRequestError, "Cannot generate path without an ID" unless persisted?
|
49
|
+
list_id = id.is_a?(Hash) ? (id[:list_id] || id["list_id"]) : id
|
50
|
+
"#{self.class.resource_path}/#{list_id}"
|
51
|
+
end
|
52
|
+
|
53
|
+
# Override the default id extraction for API paths
|
54
|
+
def id_for_path
|
55
|
+
return nil unless persisted?
|
56
|
+
id.is_a?(Hash) ? (id[:list_id] || id["list_id"]) : id
|
57
|
+
end
|
58
|
+
|
59
|
+
# Override save to handle nested ID
|
60
|
+
def save(**)
|
61
|
+
raise InvalidRequestError, "Cannot save a list without an ID" unless persisted?
|
62
|
+
return self unless changed?
|
63
|
+
|
64
|
+
list_id = id.is_a?(Hash) ? (id[:list_id] || id["list_id"]) : id
|
65
|
+
self.class.update(list_id, changed_attributes, **)
|
66
|
+
end
|
67
|
+
|
68
|
+
# Lists cannot be deleted via API
|
69
|
+
def destroy(**opts)
|
70
|
+
raise NotImplementedError, "Lists cannot be deleted via the Attio API"
|
71
|
+
end
|
72
|
+
|
73
|
+
# Get all entries in this list
|
74
|
+
def entries(params = {}, **opts)
|
75
|
+
list_id = id.is_a?(Hash) ? (id[:list_id] || id["list_id"]) : id
|
76
|
+
client = Attio.client(api_key: opts[:api_key])
|
77
|
+
# Use POST query endpoint to get entries
|
78
|
+
response = client.post("lists/#{list_id}/entries/query", params)
|
79
|
+
response["data"] || []
|
80
|
+
end
|
81
|
+
|
82
|
+
# Add a record to this list
|
83
|
+
def add_record(record_id, **opts)
|
84
|
+
list_id = id.is_a?(Hash) ? id["list_id"] : id
|
85
|
+
client = Attio.client(api_key: opts[:api_key])
|
86
|
+
|
87
|
+
# The API expects parent_record_id, parent_object, and entry_values
|
88
|
+
request_data = {
|
89
|
+
data: {
|
90
|
+
parent_record_id: record_id,
|
91
|
+
parent_object: object, # Get the parent object from the list
|
92
|
+
entry_values: {}
|
93
|
+
}
|
94
|
+
}
|
95
|
+
|
96
|
+
response = client.post("lists/#{list_id}/entries", request_data)
|
97
|
+
# Return the entry data
|
98
|
+
response["data"] || response
|
99
|
+
end
|
100
|
+
|
101
|
+
# Remove a record from this list
|
102
|
+
def remove_record(entry_id, **opts)
|
103
|
+
list_id = id.is_a?(Hash) ? id["list_id"] : id
|
104
|
+
client = Attio.client(api_key: opts[:api_key])
|
105
|
+
client.delete("lists/#{list_id}/entries/#{entry_id}")
|
106
|
+
end
|
107
|
+
|
108
|
+
# Check if a record is in this list
|
109
|
+
def contains_record?(record_id, **)
|
110
|
+
entries({record_id: record_id}, **).any?
|
111
|
+
end
|
112
|
+
|
113
|
+
# Get the count of entries
|
114
|
+
def entry_count(**)
|
115
|
+
# Just get the entries and count them
|
116
|
+
entries(**).length
|
117
|
+
end
|
118
|
+
|
119
|
+
# Convert list to hash representation
|
120
|
+
# @return [Hash] List data as a hash
|
121
|
+
def to_h
|
122
|
+
super.merge(
|
123
|
+
api_slug: api_slug,
|
124
|
+
name: name,
|
125
|
+
object_id: attio_object_id,
|
126
|
+
object_api_slug: object_api_slug,
|
127
|
+
created_by_actor: created_by_actor,
|
128
|
+
workspace_id: workspace_id,
|
129
|
+
workspace_access: workspace_access
|
130
|
+
).compact
|
131
|
+
end
|
132
|
+
|
133
|
+
class << self
|
134
|
+
# Override retrieve to handle complex IDs
|
135
|
+
def retrieve(id, **opts)
|
136
|
+
list_id = id.is_a?(Hash) ? id["list_id"] : id
|
137
|
+
response = execute_request(:GET, "#{resource_path}/#{list_id}", {}, opts)
|
138
|
+
new(response["data"] || response, opts)
|
139
|
+
end
|
140
|
+
|
141
|
+
# Override create to handle keyword arguments properly
|
142
|
+
def create(**kwargs)
|
143
|
+
# Extract options from kwargs
|
144
|
+
opts = {}
|
145
|
+
opts[:api_key] = kwargs.delete(:api_key) if kwargs.key?(:api_key)
|
146
|
+
|
147
|
+
prepared_params = prepare_params_for_create(kwargs)
|
148
|
+
response = execute_request(:POST, resource_path, prepared_params, opts)
|
149
|
+
new(response["data"] || response, opts)
|
150
|
+
end
|
151
|
+
|
152
|
+
# Override create to handle special parameters
|
153
|
+
def prepare_params_for_create(params)
|
154
|
+
validate_object_identifier!(params[:object])
|
155
|
+
|
156
|
+
# Generate api_slug from name if not provided
|
157
|
+
api_slug = params[:api_slug] || params[:name].downcase.gsub(/[^a-z0-9]+/, "_")
|
158
|
+
|
159
|
+
{
|
160
|
+
data: {
|
161
|
+
name: params[:name],
|
162
|
+
parent_object: params[:object],
|
163
|
+
api_slug: api_slug,
|
164
|
+
workspace_access: params[:workspace_access] || "full-access",
|
165
|
+
workspace_member_access: params[:workspace_member_access] || [],
|
166
|
+
filters: params[:filters]
|
167
|
+
}.compact
|
168
|
+
}
|
169
|
+
end
|
170
|
+
|
171
|
+
# Override update to handle data wrapper
|
172
|
+
def prepare_params_for_update(params)
|
173
|
+
{
|
174
|
+
data: params
|
175
|
+
}
|
176
|
+
end
|
177
|
+
|
178
|
+
# Find list by API slug
|
179
|
+
def find_by_slug(slug, **opts)
|
180
|
+
list(**opts).find { |lst| lst.api_slug == slug } ||
|
181
|
+
raise(NotFoundError, "List with slug '#{slug}' not found")
|
182
|
+
end
|
183
|
+
|
184
|
+
# Get lists for a specific object
|
185
|
+
def for_object(object, params = {}, **)
|
186
|
+
list(params.merge(object: object), **)
|
187
|
+
end
|
188
|
+
|
189
|
+
private
|
190
|
+
|
191
|
+
def validate_object_identifier!(object)
|
192
|
+
raise ArgumentError, "Object identifier is required" if object.nil? || object.to_s.empty?
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|
@@ -0,0 +1,113 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "../api_resource"
|
4
|
+
|
5
|
+
module Attio
|
6
|
+
# Provides metadata about the current API token and workspace
|
7
|
+
class Meta < APIResource
|
8
|
+
# Meta only supports the identify endpoint (no CRUD operations)
|
9
|
+
|
10
|
+
def self.resource_path
|
11
|
+
"self"
|
12
|
+
end
|
13
|
+
|
14
|
+
# Get information about the current token and workspace
|
15
|
+
def self.identify(**opts)
|
16
|
+
response = execute_request(:GET, resource_path, {}, opts)
|
17
|
+
new(response["data"] || response, opts)
|
18
|
+
end
|
19
|
+
|
20
|
+
class << self
|
21
|
+
# Convenient aliases
|
22
|
+
alias_method :self, :identify
|
23
|
+
alias_method :current, :identify
|
24
|
+
end
|
25
|
+
|
26
|
+
# Define attribute accessors
|
27
|
+
attr_attio :workspace, :token, :actor
|
28
|
+
|
29
|
+
# Convenience methods for workspace info
|
30
|
+
def workspace_id
|
31
|
+
workspace&.dig(:id)
|
32
|
+
end
|
33
|
+
|
34
|
+
# Get the workspace name
|
35
|
+
# @return [String, nil] The workspace name
|
36
|
+
def workspace_name
|
37
|
+
workspace&.dig(:name)
|
38
|
+
end
|
39
|
+
|
40
|
+
# Get the workspace slug
|
41
|
+
# @return [String, nil] The workspace slug
|
42
|
+
def workspace_slug
|
43
|
+
workspace&.dig(:slug)
|
44
|
+
end
|
45
|
+
|
46
|
+
# Convenience methods for token info
|
47
|
+
def token_id
|
48
|
+
token&.dig(:id)
|
49
|
+
end
|
50
|
+
|
51
|
+
# Get the token name
|
52
|
+
# @return [String, nil] The token name
|
53
|
+
def token_name
|
54
|
+
token&.dig(:name)
|
55
|
+
end
|
56
|
+
|
57
|
+
# Get the token type
|
58
|
+
# @return [String, nil] The token type
|
59
|
+
def token_type
|
60
|
+
token&.dig(:type)
|
61
|
+
end
|
62
|
+
|
63
|
+
# Get the token's OAuth scopes
|
64
|
+
# @return [Array<String>] Array of scope strings
|
65
|
+
def scopes
|
66
|
+
token&.dig(:scopes) || []
|
67
|
+
end
|
68
|
+
|
69
|
+
# Check if token has a specific scope
|
70
|
+
def has_scope?(scope)
|
71
|
+
scope_str = scope.to_s.tr("_", ":")
|
72
|
+
scopes.include?(scope_str)
|
73
|
+
end
|
74
|
+
|
75
|
+
# Check read/write permissions
|
76
|
+
def can_read?(resource)
|
77
|
+
has_scope?("#{resource}:read") || has_scope?("#{resource}:read-write")
|
78
|
+
end
|
79
|
+
|
80
|
+
def can_write?(resource)
|
81
|
+
has_scope?("#{resource}:write") || has_scope?("#{resource}:read-write")
|
82
|
+
end
|
83
|
+
|
84
|
+
# Meta is read-only
|
85
|
+
def immutable?
|
86
|
+
true
|
87
|
+
end
|
88
|
+
|
89
|
+
# Override save to raise error since meta is read-only
|
90
|
+
def save(**opts)
|
91
|
+
raise InvalidRequestError, "Meta information is read-only"
|
92
|
+
end
|
93
|
+
|
94
|
+
# Override destroy to raise error since meta is read-only
|
95
|
+
def destroy(**opts)
|
96
|
+
raise InvalidRequestError, "Meta information is read-only"
|
97
|
+
end
|
98
|
+
|
99
|
+
private
|
100
|
+
|
101
|
+
def to_h
|
102
|
+
{
|
103
|
+
workspace: workspace,
|
104
|
+
token: token,
|
105
|
+
actor: actor
|
106
|
+
}.compact
|
107
|
+
end
|
108
|
+
|
109
|
+
def inspect
|
110
|
+
"#<#{self.class.name}:#{object_id} workspace=#{workspace_slug.inspect} token=#{token_name.inspect}>"
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|