attio-rails 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.
data/Gemfile ADDED
@@ -0,0 +1,20 @@
1
+ source "https://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in attio-rails.gemspec
4
+ gemspec
5
+
6
+ gem "rake", "~> 13.0"
7
+
8
+ group :development, :test do
9
+ gem "rspec", "~> 3.12"
10
+ gem "rspec-rails", "~> 6.0"
11
+ gem "rails", "~> 7.0"
12
+ gem "sqlite3", "~> 1.4"
13
+ gem "simplecov", "~> 0.22"
14
+ gem "simplecov-console", "~> 0.9"
15
+ gem "rubocop", "~> 1.50"
16
+ gem "rubocop-rails", "~> 2.19"
17
+ gem "rubocop-rspec", "~> 2.19"
18
+ gem "pry", "~> 0.14"
19
+ gem "webmock", "~> 3.18"
20
+ end
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2025 Ernest Sim
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,243 @@
1
+ # Attio Rails
2
+
3
+ [![Documentation](https://img.shields.io/badge/docs-yard-blue.svg)](https://idl3.github.io/attio-rails)
4
+ [![Gem Version](https://badge.fury.io/rb/attio-rails.svg)](https://badge.fury.io/rb/attio-rails)
5
+ [![CI](https://github.com/idl3/attio-rails/actions/workflows/ci.yml/badge.svg)](https://github.com/idl3/attio-rails/actions/workflows/ci.yml)
6
+
7
+ Rails integration for the [Attio](https://github.com/idl3/attio) Ruby client. This gem provides Rails-specific features including ActiveRecord model synchronization, generators, and background job integration.
8
+
9
+ ## Installation
10
+
11
+ Add this line to your application's Gemfile:
12
+
13
+ ```ruby
14
+ gem 'attio-rails', '~> 0.1.0'
15
+ ```
16
+
17
+ And then execute:
18
+
19
+ ```bash
20
+ bundle install
21
+ rails generate attio:install
22
+ ```
23
+
24
+ ## Configuration
25
+
26
+ After running the install generator, configure Attio in `config/initializers/attio.rb`:
27
+
28
+ ```ruby
29
+ Attio::Rails.configure do |config|
30
+ config.api_key = ENV['ATTIO_API_KEY']
31
+ config.async = true # Use ActiveJob for syncing
32
+ config.queue = :default # ActiveJob queue name
33
+ config.logger = Rails.logger
34
+ end
35
+ ```
36
+
37
+ ## Usage
38
+
39
+ ### ActiveRecord Integration
40
+
41
+ Add the `Attio::Rails::Syncable` concern to your models:
42
+
43
+ ```ruby
44
+ class User < ApplicationRecord
45
+ include Attio::Rails::Syncable
46
+
47
+ attio_syncable object: 'people',
48
+ attributes: [:email, :first_name, :last_name],
49
+ identifier: :email
50
+
51
+ # Optional callbacks
52
+ before_attio_sync :prepare_data
53
+ after_attio_sync :log_sync
54
+
55
+ private
56
+
57
+ def prepare_data
58
+ # Prepare data before syncing
59
+ end
60
+
61
+ def log_sync(response)
62
+ Rails.logger.info "Synced to Attio: #{response['id']}"
63
+ end
64
+ end
65
+ ```
66
+
67
+ ### Manual Syncing
68
+
69
+ ```ruby
70
+ # Sync a single record
71
+ user = User.find(1)
72
+ user.sync_to_attio
73
+
74
+ # Sync multiple records
75
+ User.where(active: true).find_each(&:sync_to_attio)
76
+
77
+ # Async sync (requires ActiveJob)
78
+ user.sync_to_attio_later
79
+ ```
80
+
81
+ ### Custom Field Mapping
82
+
83
+ ```ruby
84
+ class Company < ApplicationRecord
85
+ include Attio::Rails::Syncable
86
+
87
+ attio_syncable object: 'companies',
88
+ attributes: {
89
+ name: :company_name,
90
+ domain: :website_url,
91
+ employee_count: ->(c) { c.employees.count }
92
+ },
93
+ identifier: :domain
94
+ end
95
+ ```
96
+
97
+ ### Batch Operations
98
+
99
+ ```ruby
100
+ # Sync multiple records efficiently
101
+ Attio::Rails::BatchSync.perform(
102
+ User.where(updated_at: 1.day.ago..),
103
+ object: 'people'
104
+ )
105
+ ```
106
+
107
+ ### ActiveJob Integration
108
+
109
+ The gem automatically uses ActiveJob for background syncing when configured:
110
+
111
+ ```ruby
112
+ class User < ApplicationRecord
113
+ include Attio::Rails::Syncable
114
+
115
+ attio_syncable object: 'people', async: true
116
+
117
+ # Automatically syncs in background after save
118
+ after_commit :sync_to_attio_later
119
+ end
120
+ ```
121
+
122
+ ### Generator
123
+
124
+ The install generator creates:
125
+ - Configuration initializer at `config/initializers/attio.rb`
126
+ - ActiveJob class at `app/jobs/attio_sync_job.rb`
127
+ - Migration for tracking sync status (optional)
128
+
129
+ ```bash
130
+ rails generate attio:install
131
+ rails generate attio:install --skip-job # Skip job creation
132
+ rails generate attio:install --skip-migration # Skip migration
133
+ ```
134
+
135
+ ## Advanced Features
136
+
137
+ ### Conditional Syncing
138
+
139
+ ```ruby
140
+ class User < ApplicationRecord
141
+ include Attio::Rails::Syncable
142
+
143
+ attio_syncable object: 'people',
144
+ if: :should_sync_to_attio?
145
+
146
+ def should_sync_to_attio?
147
+ confirmed? && !deleted?
148
+ end
149
+ end
150
+ ```
151
+
152
+ ### Error Handling
153
+
154
+ ```ruby
155
+ class User < ApplicationRecord
156
+ include Attio::Rails::Syncable
157
+
158
+ attio_syncable object: 'people',
159
+ on_error: :handle_sync_error
160
+
161
+ def handle_sync_error(error)
162
+ Rails.logger.error "Attio sync failed: #{error.message}"
163
+ Sentry.capture_exception(error)
164
+ end
165
+ end
166
+ ```
167
+
168
+ ### Custom Transformations
169
+
170
+ ```ruby
171
+ class User < ApplicationRecord
172
+ include Attio::Rails::Syncable
173
+
174
+ attio_syncable object: 'people',
175
+ transform: :transform_for_attio
176
+
177
+ def transform_for_attio(attributes)
178
+ attributes.merge(
179
+ full_name: "#{first_name} #{last_name}",
180
+ tags: user_tags.pluck(:name)
181
+ )
182
+ end
183
+ end
184
+ ```
185
+
186
+ ## Testing
187
+
188
+ The gem includes RSpec helpers for testing:
189
+
190
+ ```ruby
191
+ # In spec/rails_helper.rb
192
+ require 'attio/rails/rspec'
193
+
194
+ # In your specs
195
+ RSpec.describe User do
196
+ include Attio::Rails::RSpec::Helpers
197
+
198
+ it 'syncs to Attio' do
199
+ user = create(:user)
200
+
201
+ expect_attio_sync(object: 'people') do
202
+ user.sync_to_attio
203
+ end
204
+ end
205
+ end
206
+ ```
207
+
208
+ ## Development
209
+
210
+ After checking out the repo:
211
+
212
+ ```bash
213
+ bundle install
214
+ cd spec/dummy
215
+ rails db:create db:migrate
216
+ cd ../..
217
+ bundle exec rspec
218
+ ```
219
+
220
+ To run tests against different Rails versions:
221
+
222
+ ```bash
223
+ RAILS_VERSION=7.1 bundle update
224
+ bundle exec rspec
225
+
226
+ RAILS_VERSION=7.0 bundle update
227
+ bundle exec rspec
228
+ ```
229
+
230
+ ## Contributing
231
+
232
+ Bug reports and pull requests are welcome on GitHub at https://github.com/idl3/attio-rails. Please read our [Contributing Guidelines](CONTRIBUTING.md) and [Code of Conduct](CODE_OF_CONDUCT.md).
233
+
234
+ ## License
235
+
236
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
237
+
238
+ ## Support
239
+
240
+ - 📖 [Documentation](https://idl3.github.io/attio-rails)
241
+ - 🐛 [Issues](https://github.com/idl3/attio-rails/issues)
242
+ - 💬 [Discussions](https://github.com/idl3/attio-rails/discussions)
243
+ - 📦 [Main Attio Gem](https://github.com/idl3/attio)
data/Rakefile ADDED
@@ -0,0 +1,29 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+ require "rubocop/rake_task"
4
+ require "yard"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+ RuboCop::RakeTask.new
8
+
9
+ YARD::Rake::YardocTask.new do |t|
10
+ t.files = ['lib/**/*.rb']
11
+ t.options = ['--markup-provider=redcarpet', '--markup=markdown', '--protected', '--private']
12
+ t.stats_options = ['--list-undoc']
13
+ end
14
+
15
+ namespace :yard do
16
+ desc "Generate documentation and serve it with live reloading"
17
+ task :server do
18
+ sh "bundle exec yard server --reload"
19
+ end
20
+
21
+ desc "Generate documentation for GitHub Pages"
22
+ task :gh_pages do
23
+ sh "bundle exec yard --output-dir docs"
24
+ # Create .nojekyll file for GitHub Pages
25
+ File.open("docs/.nojekyll", "w") {}
26
+ end
27
+ end
28
+
29
+ task :default => [:spec, :rubocop]
data/SECURITY.md ADDED
@@ -0,0 +1,63 @@
1
+ # Security Policy
2
+
3
+ ## Supported Versions
4
+
5
+ We release patches for security vulnerabilities. Which versions are eligible depends on the CVSS v3.0 Rating:
6
+
7
+ | Version | Supported |
8
+ | ------- | ------------------ |
9
+ | 1.x.x | :white_check_mark: |
10
+ | < 1.0 | :x: |
11
+
12
+ ## Reporting a Vulnerability
13
+
14
+ If you discover a security vulnerability within this project, please follow these steps:
15
+
16
+ 1. **Do NOT** create a public GitHub issue
17
+ 2. Send details to the maintainers through GitHub Security Advisories
18
+ 3. Include the following in your report:
19
+ - Description of the vulnerability
20
+ - Steps to reproduce
21
+ - Possible impact
22
+ - Suggested fix (if any)
23
+
24
+ ### What to expect
25
+
26
+ - Acknowledgment of your report within 48 hours
27
+ - Regular updates on our progress
28
+ - Credit for responsible disclosure (unless you prefer to remain anonymous)
29
+
30
+ ## Security Best Practices
31
+
32
+ When using this gem:
33
+
34
+ 1. **API Key Management**
35
+ - Never commit API keys to version control
36
+ - Use Rails credentials or environment variables
37
+ - Rotate API keys regularly
38
+ - Use different keys for different environments
39
+
40
+ 2. **Rails Security**
41
+ - Keep Rails and dependencies updated
42
+ - Follow Rails security best practices
43
+ - Use strong parameters
44
+ - Implement proper authentication and authorization
45
+
46
+ 3. **Data Handling**
47
+ - Be cautious with sensitive data in logs
48
+ - Use Rails encrypted credentials
49
+ - Implement proper error handling
50
+ - Sanitize user input before syncing to Attio
51
+
52
+ ## Security Features
53
+
54
+ This gem includes:
55
+ - Integration with Rails security features
56
+ - Automatic API key masking in logs
57
+ - SSL/TLS verification by default
58
+ - Input validation and sanitization
59
+ - Safe handling of ActiveRecord callbacks
60
+
61
+ ## Contact
62
+
63
+ For security concerns, please use GitHub Security Advisories or contact the maintainers directly through GitHub.
@@ -0,0 +1,42 @@
1
+ require_relative 'lib/attio/rails/version'
2
+
3
+ Gem::Specification.new do |spec|
4
+ spec.name = "attio-rails"
5
+ spec.version = Attio::Rails::VERSION
6
+ spec.authors = ["Ernest Sim"]
7
+ spec.email = ["ernest.codes@gmail.com"]
8
+
9
+ spec.summary = %q{Rails integration for the Attio API client}
10
+ spec.description = %q{Rails-specific features and integrations for the Attio Ruby client, including model concerns and generators}
11
+ spec.homepage = "https://github.com/idl3/attio-rails"
12
+ spec.license = "MIT"
13
+ spec.required_ruby_version = Gem::Requirement.new(">= 3.0.0")
14
+
15
+ spec.metadata["allowed_push_host"] = "https://rubygems.org"
16
+
17
+ spec.metadata["homepage_uri"] = spec.homepage
18
+ spec.metadata["source_code_uri"] = spec.homepage
19
+ spec.metadata["changelog_uri"] = "#{spec.homepage}/blob/main/CHANGELOG.md"
20
+ spec.metadata["documentation_uri"] = "https://idl3.github.io/attio-rails"
21
+
22
+ # Specify which files should be added to the gem when it is released.
23
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
24
+ spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
25
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
26
+ end
27
+ spec.bindir = "exe"
28
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
29
+ spec.require_paths = ["lib"]
30
+
31
+ spec.add_dependency "attio", "~> 0.1", ">= 0.1.1"
32
+ spec.add_dependency "rails", ">= 6.1", "< 8.0"
33
+
34
+ spec.add_development_dependency "yard", "~> 0.9"
35
+ spec.add_development_dependency "redcarpet", "~> 3.5"
36
+ spec.add_development_dependency "rspec", "~> 3.12"
37
+ spec.add_development_dependency "rubocop", "~> 1.50"
38
+ spec.add_development_dependency "simplecov", "~> 0.22"
39
+ spec.add_development_dependency "rake", "~> 13.0"
40
+ spec.add_development_dependency "bundler-audit", "~> 0.9"
41
+ spec.add_development_dependency "danger", "~> 9.4"
42
+ end
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "attio/rails"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
data/docs/index.html ADDED
@@ -0,0 +1,91 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Attio Rails Integration Documentation</title>
7
+ <style>
8
+ body {
9
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
10
+ line-height: 1.6;
11
+ color: #333;
12
+ max-width: 800px;
13
+ margin: 0 auto;
14
+ padding: 20px;
15
+ background: #f8f9fa;
16
+ }
17
+ .container {
18
+ background: white;
19
+ padding: 40px;
20
+ border-radius: 8px;
21
+ box-shadow: 0 2px 10px rgba(0,0,0,0.1);
22
+ }
23
+ h1 {
24
+ color: #2c3e50;
25
+ border-bottom: 2px solid #3498db;
26
+ padding-bottom: 10px;
27
+ }
28
+ .notice {
29
+ background: #e3f2fd;
30
+ border-left: 4px solid #2196f3;
31
+ padding: 15px;
32
+ margin: 20px 0;
33
+ border-radius: 4px;
34
+ }
35
+ .button {
36
+ display: inline-block;
37
+ background: #3498db;
38
+ color: white;
39
+ padding: 10px 20px;
40
+ text-decoration: none;
41
+ border-radius: 4px;
42
+ margin: 10px 10px 10px 0;
43
+ transition: background 0.3s;
44
+ }
45
+ .button:hover {
46
+ background: #2980b9;
47
+ }
48
+ pre {
49
+ background: #f4f4f4;
50
+ border: 1px solid #ddd;
51
+ border-radius: 4px;
52
+ padding: 15px;
53
+ overflow-x: auto;
54
+ }
55
+ </style>
56
+ </head>
57
+ <body>
58
+ <div class="container">
59
+ <h1>Attio Rails Integration Documentation</h1>
60
+
61
+ <div class="notice">
62
+ <strong>Documentation Status:</strong> This is a placeholder page. The full API documentation will be generated automatically when the gem is built.
63
+ </div>
64
+
65
+ <h2>Quick Start</h2>
66
+ <p>Add the gem to your Gemfile:</p>
67
+ <pre><code>gem 'attio-rails'</code></pre>
68
+
69
+ <p>Run the installer:</p>
70
+ <pre><code>rails generate attio:install</code></pre>
71
+
72
+ <h2>Documentation Links</h2>
73
+ <a href="./file.README.html" class="button">README</a>
74
+ <a href="./Attio/Rails.html" class="button">API Documentation</a>
75
+ <a href="./method_list.html" class="button">Method List</a>
76
+ <a href="./class_list.html" class="button">Class List</a>
77
+
78
+ <h2>GitHub Repository</h2>
79
+ <p>Visit the <a href="https://github.com/your-username/attio-rails">GitHub repository</a> for source code, issues, and contributions.</p>
80
+
81
+ <h2>Generate Documentation Locally</h2>
82
+ <p>To generate the full documentation locally:</p>
83
+ <pre><code>bundle exec rake yard
84
+ bundle exec yard server</code></pre>
85
+
86
+ <div class="notice">
87
+ <strong>Note:</strong> This documentation is automatically updated when changes are pushed to the main branch.
88
+ </div>
89
+ </div>
90
+ </body>
91
+ </html>
@@ -0,0 +1,132 @@
1
+ module Attio
2
+ module Rails
3
+ module Concerns
4
+ module Syncable
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ after_commit :sync_to_attio, on: [:create, :update], if: :should_sync_to_attio?
9
+ after_commit :remove_from_attio, on: :destroy, if: :should_remove_from_attio?
10
+
11
+ class_attribute :attio_object_type
12
+ class_attribute :attio_attribute_mapping
13
+ class_attribute :attio_sync_conditions
14
+ class_attribute :attio_identifier_attribute
15
+ end
16
+
17
+ class_methods do
18
+ def syncs_with_attio(object_type, mapping = {}, options = {})
19
+ self.attio_object_type = object_type
20
+ self.attio_attribute_mapping = mapping
21
+ self.attio_sync_conditions = options[:if] || -> { true }
22
+ self.attio_identifier_attribute = options[:identifier] || :id
23
+ end
24
+
25
+ def skip_attio_sync
26
+ skip_callback :commit, :after, :sync_to_attio
27
+ skip_callback :commit, :after, :remove_from_attio
28
+ end
29
+ end
30
+
31
+ def sync_to_attio
32
+ if Attio::Rails.background_sync?
33
+ AttioSyncJob.perform_later(
34
+ model_name: self.class.name,
35
+ model_id: id,
36
+ action: :sync
37
+ )
38
+ else
39
+ sync_to_attio_now
40
+ end
41
+ rescue StandardError => e
42
+ Attio::Rails.logger.error "Failed to sync to Attio: #{e.message}"
43
+ raise if ::Rails.env.development?
44
+ end
45
+
46
+ def sync_to_attio_now
47
+ client = Attio::Rails.client
48
+ attributes = attio_attributes
49
+
50
+ if attio_record_id.present?
51
+ client.records.update(
52
+ object: attio_object_type,
53
+ id: attio_record_id,
54
+ data: { values: attributes }
55
+ )
56
+ else
57
+ response = client.records.create(
58
+ object: attio_object_type,
59
+ data: { values: attributes }
60
+ )
61
+ update_column(:attio_record_id, response['data']['id']) if respond_to?(:attio_record_id=)
62
+ end
63
+ end
64
+
65
+ def remove_from_attio
66
+ if Attio::Rails.background_sync?
67
+ AttioSyncJob.perform_later(
68
+ model_name: self.class.name,
69
+ model_id: id,
70
+ action: :delete,
71
+ attio_record_id: attio_record_id
72
+ )
73
+ else
74
+ remove_from_attio_now
75
+ end
76
+ rescue StandardError => e
77
+ Attio::Rails.logger.error "Failed to remove from Attio: #{e.message}"
78
+ raise if ::Rails.env.development?
79
+ end
80
+
81
+ def remove_from_attio_now
82
+ return unless attio_record_id.present?
83
+
84
+ client = Attio::Rails.client
85
+ client.records.delete(
86
+ object: attio_object_type,
87
+ id: attio_record_id
88
+ )
89
+ end
90
+
91
+ def attio_attributes
92
+ return {} unless attio_attribute_mapping
93
+
94
+ attio_attribute_mapping.each_with_object({}) do |(attio_key, local_key), hash|
95
+ value = case local_key
96
+ when Proc
97
+ local_key.call(self)
98
+ when Symbol, String
99
+ send(local_key)
100
+ else
101
+ local_key
102
+ end
103
+ hash[attio_key] = value unless value.nil?
104
+ end
105
+ end
106
+
107
+ def should_sync_to_attio?
108
+ return false unless Attio::Rails.sync_enabled?
109
+ return false unless attio_object_type.present? && attio_attribute_mapping.present?
110
+
111
+ condition = attio_sync_conditions
112
+ case condition
113
+ when Proc
114
+ instance_exec(&condition)
115
+ when Symbol, String
116
+ send(condition)
117
+ else
118
+ true
119
+ end
120
+ end
121
+
122
+ def should_remove_from_attio?
123
+ attio_record_id.present? && Attio::Rails.sync_enabled?
124
+ end
125
+
126
+ def attio_identifier
127
+ send(attio_identifier_attribute)
128
+ end
129
+ end
130
+ end
131
+ end
132
+ end