attio-rails 0.1.1 → 0.2.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 +4 -4
- data/.github/workflows/{release.yml → build-and-publish.yml} +3 -13
- data/.github/workflows/ci.yml +1 -1
- data/.github/workflows/docs.yml +2 -2
- data/.rubocop.yml +381 -35
- data/.rubocop_todo.yml +2 -0
- data/CHANGELOG.md +40 -1
- data/CONCEPTS.md +448 -0
- data/Gemfile +14 -8
- data/Rakefile +8 -6
- data/attio-rails.gemspec +7 -18
- data/lib/attio/rails/concerns/syncable.rb +27 -16
- data/lib/attio/rails/configuration.rb +10 -3
- data/lib/attio/rails/railtie.rb +7 -3
- data/lib/attio/rails/rspec/helpers.rb +209 -0
- data/lib/attio/rails/rspec/matchers.rb +145 -0
- data/lib/attio/rails/rspec.rb +9 -0
- data/lib/attio/rails/version.rb +3 -1
- data/lib/attio/rails.rb +3 -0
- data/lib/generators/attio/install/install_generator.rb +25 -23
- data/lib/generators/attio/install/templates/attio.rb +5 -3
- data/lib/generators/attio/install/templates/attio_sync_job.rb +11 -11
- data/lib/generators/attio/install/templates/{migration.rb → migration.rb.erb} +1 -1
- data/test_basic.rb +15 -14
- metadata +15 -175
data/CONCEPTS.md
ADDED
@@ -0,0 +1,448 @@
|
|
1
|
+
# Attio Rails - Concepts & Architecture
|
2
|
+
|
3
|
+
## Table of Contents
|
4
|
+
- [Core Concepts](#core-concepts)
|
5
|
+
- [Architecture Overview](#architecture-overview)
|
6
|
+
- [Sync Flow](#sync-flow)
|
7
|
+
- [Batch Processing](#batch-processing)
|
8
|
+
- [Error Handling & Retry Strategy](#error-handling--retry-strategy)
|
9
|
+
- [Testing Strategy](#testing-strategy)
|
10
|
+
|
11
|
+
## Core Concepts
|
12
|
+
|
13
|
+
### 1. ActiveRecord Integration Pattern
|
14
|
+
|
15
|
+
The gem uses the **Concern pattern** to mix functionality into ActiveRecord models. This provides a clean, Rails-idiomatic interface:
|
16
|
+
|
17
|
+
```mermaid
|
18
|
+
graph TD
|
19
|
+
A[ActiveRecord Model] -->|includes| B[Attio::Rails::Concerns::Syncable]
|
20
|
+
B -->|provides| C[Sync Methods]
|
21
|
+
B -->|provides| D[Callbacks]
|
22
|
+
B -->|provides| E[Attribute Mapping]
|
23
|
+
|
24
|
+
C --> F[sync_to_attio_now]
|
25
|
+
C --> G[sync_to_attio_later]
|
26
|
+
C --> H[remove_from_attio]
|
27
|
+
|
28
|
+
D --> I[before_attio_sync]
|
29
|
+
D --> J[after_attio_sync]
|
30
|
+
D --> K[ActiveRecord Callbacks]
|
31
|
+
```
|
32
|
+
|
33
|
+
### 2. Attribute Mapping System
|
34
|
+
|
35
|
+
The attribute mapping system supports multiple mapping strategies:
|
36
|
+
|
37
|
+
```mermaid
|
38
|
+
graph LR
|
39
|
+
A[Model Attributes] --> B{Mapping Type}
|
40
|
+
B -->|Symbol| C[Method Call]
|
41
|
+
B -->|String| D[Method Call]
|
42
|
+
B -->|Proc/Lambda| E[Dynamic Evaluation]
|
43
|
+
B -->|Static Value| F[Direct Value]
|
44
|
+
|
45
|
+
C --> G[Attio Attributes]
|
46
|
+
D --> G
|
47
|
+
E --> G
|
48
|
+
F --> G
|
49
|
+
```
|
50
|
+
|
51
|
+
**Example mappings:**
|
52
|
+
```ruby
|
53
|
+
{
|
54
|
+
email: :email_address, # Symbol -> calls model.email_address
|
55
|
+
name: "full_name", # String -> calls model.full_name
|
56
|
+
type: "customer", # Static -> always "customer"
|
57
|
+
count: ->(m) { m.items.count } # Lambda -> evaluated dynamically
|
58
|
+
}
|
59
|
+
```
|
60
|
+
|
61
|
+
## Architecture Overview
|
62
|
+
|
63
|
+
### System Components
|
64
|
+
|
65
|
+
```mermaid
|
66
|
+
graph TB
|
67
|
+
subgraph "Rails Application"
|
68
|
+
A[ActiveRecord Model]
|
69
|
+
B[ActiveJob Queue]
|
70
|
+
end
|
71
|
+
|
72
|
+
subgraph "Attio Rails Gem"
|
73
|
+
C[Syncable Concern]
|
74
|
+
D[AttioSyncJob]
|
75
|
+
E[BatchSync]
|
76
|
+
F[Configuration]
|
77
|
+
end
|
78
|
+
|
79
|
+
subgraph "External"
|
80
|
+
G[Attio API]
|
81
|
+
end
|
82
|
+
|
83
|
+
A -->|includes| C
|
84
|
+
C -->|enqueues| D
|
85
|
+
C -->|uses| E
|
86
|
+
D -->|processes| B
|
87
|
+
E -->|bulk operations| G
|
88
|
+
D -->|sync/delete| G
|
89
|
+
C -->|immediate sync| G
|
90
|
+
F -->|configures| C
|
91
|
+
F -->|configures| D
|
92
|
+
```
|
93
|
+
|
94
|
+
## Sync Flow
|
95
|
+
|
96
|
+
### Automatic Sync Lifecycle
|
97
|
+
|
98
|
+
```mermaid
|
99
|
+
sequenceDiagram
|
100
|
+
participant User
|
101
|
+
participant Model
|
102
|
+
participant Syncable
|
103
|
+
participant AttioSyncJob
|
104
|
+
participant AttioAPI
|
105
|
+
|
106
|
+
User->>Model: create/update/destroy
|
107
|
+
Model->>Syncable: after_commit callback
|
108
|
+
|
109
|
+
alt Sync Enabled & Conditions Met
|
110
|
+
alt Background Sync
|
111
|
+
Syncable->>AttioSyncJob: enqueue job
|
112
|
+
AttioSyncJob-->>AttioSyncJob: process async
|
113
|
+
AttioSyncJob->>AttioAPI: sync data
|
114
|
+
AttioAPI-->>AttioSyncJob: response
|
115
|
+
AttioSyncJob->>Model: update attio_record_id
|
116
|
+
else Immediate Sync
|
117
|
+
Syncable->>AttioAPI: sync data
|
118
|
+
AttioAPI-->>Syncable: response
|
119
|
+
Syncable->>Model: update attio_record_id
|
120
|
+
end
|
121
|
+
else Sync Disabled or Conditions Not Met
|
122
|
+
Syncable-->>Model: skip sync
|
123
|
+
end
|
124
|
+
```
|
125
|
+
|
126
|
+
### Manual Sync Options
|
127
|
+
|
128
|
+
```mermaid
|
129
|
+
graph TD
|
130
|
+
A[Manual Sync Trigger] --> B{Sync Method}
|
131
|
+
|
132
|
+
B -->|sync_to_attio_now| C[Immediate Sync]
|
133
|
+
B -->|sync_to_attio_later| D[Background Job]
|
134
|
+
B -->|sync_to_attio| E{Config Check}
|
135
|
+
|
136
|
+
E -->|background_sync=true| D
|
137
|
+
E -->|background_sync=false| C
|
138
|
+
|
139
|
+
C --> F[Direct API Call]
|
140
|
+
D --> G[Enqueue AttioSyncJob]
|
141
|
+
|
142
|
+
F --> H[Attio API]
|
143
|
+
G --> I[Job Queue]
|
144
|
+
I --> H
|
145
|
+
```
|
146
|
+
|
147
|
+
## Batch Processing
|
148
|
+
|
149
|
+
### BatchSync Flow
|
150
|
+
|
151
|
+
```mermaid
|
152
|
+
flowchart TD
|
153
|
+
A[BatchSync.perform] --> B[Initialize Results Hash]
|
154
|
+
B --> C[Process in Batches]
|
155
|
+
|
156
|
+
C --> D{Async Mode?}
|
157
|
+
|
158
|
+
D -->|Yes| E[Enqueue Batch]
|
159
|
+
D -->|No| F[Sync Batch]
|
160
|
+
|
161
|
+
E --> G[For Each Record]
|
162
|
+
G --> H[Enqueue AttioSyncJob]
|
163
|
+
|
164
|
+
F --> I[For Each Record]
|
165
|
+
I --> J{Has attio_record_id?}
|
166
|
+
|
167
|
+
J -->|Yes| K[Update Record]
|
168
|
+
J -->|No| L[Create Record]
|
169
|
+
|
170
|
+
K --> M[API Call]
|
171
|
+
L --> M
|
172
|
+
|
173
|
+
M --> N{Success?}
|
174
|
+
N -->|Yes| O[Add to success array]
|
175
|
+
N -->|No| P[Add to failed array]
|
176
|
+
|
177
|
+
H --> Q[Add to success array]
|
178
|
+
|
179
|
+
O --> R[Return Results]
|
180
|
+
P --> R
|
181
|
+
Q --> R
|
182
|
+
```
|
183
|
+
|
184
|
+
### Batch Processing Strategies
|
185
|
+
|
186
|
+
```mermaid
|
187
|
+
graph LR
|
188
|
+
A[Large Dataset] --> B[find_in_batches]
|
189
|
+
B --> C[Batch 1]
|
190
|
+
B --> D[Batch 2]
|
191
|
+
B --> E[Batch N]
|
192
|
+
|
193
|
+
C --> F{Processing Mode}
|
194
|
+
D --> F
|
195
|
+
E --> F
|
196
|
+
|
197
|
+
F -->|Async| G[Job Queue]
|
198
|
+
F -->|Sync| H[Direct Processing]
|
199
|
+
|
200
|
+
G --> I[Parallel Processing]
|
201
|
+
H --> J[Sequential Processing]
|
202
|
+
```
|
203
|
+
|
204
|
+
## Error Handling & Retry Strategy
|
205
|
+
|
206
|
+
### Error Flow
|
207
|
+
|
208
|
+
```mermaid
|
209
|
+
flowchart TD
|
210
|
+
A[Sync Operation] --> B{Success?}
|
211
|
+
|
212
|
+
B -->|Yes| C[Update Local Record]
|
213
|
+
B -->|No| D[Error Occurred]
|
214
|
+
|
215
|
+
D --> E{Has Error Handler?}
|
216
|
+
|
217
|
+
E -->|Yes| F[Call Error Handler]
|
218
|
+
E -->|No| G{Environment?}
|
219
|
+
|
220
|
+
F --> H[Custom Logic]
|
221
|
+
|
222
|
+
G -->|Development| I[Raise Error]
|
223
|
+
G -->|Production| J[Log Error]
|
224
|
+
|
225
|
+
H --> K{In Background Job?}
|
226
|
+
K -->|Yes| L[Retry Logic]
|
227
|
+
K -->|No| M[Complete]
|
228
|
+
|
229
|
+
L --> N{Retry Attempts < 3?}
|
230
|
+
N -->|Yes| O[Wait & Retry]
|
231
|
+
N -->|No| P[Dead Letter Queue]
|
232
|
+
|
233
|
+
O --> A
|
234
|
+
```
|
235
|
+
|
236
|
+
### Retry Strategy with ActiveJob
|
237
|
+
|
238
|
+
```mermaid
|
239
|
+
graph TD
|
240
|
+
A[Job Fails] --> B[Retry Mechanism]
|
241
|
+
B --> C{Attempt 1}
|
242
|
+
C -->|Fails| D[Wait 3 seconds]
|
243
|
+
D --> E{Attempt 2}
|
244
|
+
E -->|Fails| F[Wait 18 seconds]
|
245
|
+
F --> G{Attempt 3}
|
246
|
+
G -->|Fails| H[Job Failed]
|
247
|
+
G -->|Success| I[Complete]
|
248
|
+
E -->|Success| I
|
249
|
+
C -->|Success| I
|
250
|
+
|
251
|
+
style H fill:#f96
|
252
|
+
style I fill:#9f6
|
253
|
+
```
|
254
|
+
|
255
|
+
## Testing Strategy
|
256
|
+
|
257
|
+
### Test Double Architecture
|
258
|
+
|
259
|
+
```mermaid
|
260
|
+
graph TD
|
261
|
+
A[RSpec Test] --> B[Test Helpers]
|
262
|
+
|
263
|
+
B --> C[stub_attio_client]
|
264
|
+
C --> D[Mock Client]
|
265
|
+
C --> E[Mock Records API]
|
266
|
+
|
267
|
+
B --> F[expect_attio_sync]
|
268
|
+
F --> G[Expectation Setup]
|
269
|
+
|
270
|
+
B --> H[with_attio_sync_disabled]
|
271
|
+
H --> I[Temporary Config Change]
|
272
|
+
|
273
|
+
D --> J[No Real API Calls]
|
274
|
+
E --> J
|
275
|
+
|
276
|
+
J --> K[Fast Tests]
|
277
|
+
J --> L[Predictable Results]
|
278
|
+
```
|
279
|
+
|
280
|
+
### Testing Layers
|
281
|
+
|
282
|
+
```mermaid
|
283
|
+
graph TB
|
284
|
+
subgraph "Unit Tests"
|
285
|
+
A[Model Tests]
|
286
|
+
B[Concern Tests]
|
287
|
+
C[Job Tests]
|
288
|
+
end
|
289
|
+
|
290
|
+
subgraph "Integration Tests"
|
291
|
+
D[Sync Flow Tests]
|
292
|
+
E[Batch Processing Tests]
|
293
|
+
end
|
294
|
+
|
295
|
+
subgraph "Test Helpers"
|
296
|
+
F[Stubbed Client]
|
297
|
+
G[Job Assertions]
|
298
|
+
H[Config Helpers]
|
299
|
+
end
|
300
|
+
|
301
|
+
A --> F
|
302
|
+
B --> F
|
303
|
+
C --> G
|
304
|
+
D --> F
|
305
|
+
D --> G
|
306
|
+
E --> F
|
307
|
+
E --> H
|
308
|
+
```
|
309
|
+
|
310
|
+
## Configuration Cascade
|
311
|
+
|
312
|
+
### Configuration Priority
|
313
|
+
|
314
|
+
```mermaid
|
315
|
+
graph TD
|
316
|
+
A[Configuration Sources] --> B[Environment Variables]
|
317
|
+
A --> C[Initializer Config]
|
318
|
+
A --> D[Model-level Options]
|
319
|
+
A --> E[Method-level Options]
|
320
|
+
|
321
|
+
B --> F{Priority}
|
322
|
+
C --> F
|
323
|
+
D --> F
|
324
|
+
E --> F
|
325
|
+
|
326
|
+
F --> G[Final Configuration]
|
327
|
+
|
328
|
+
style E fill:#9f6
|
329
|
+
style D fill:#af9
|
330
|
+
style C fill:#cf9
|
331
|
+
style B fill:#ff9
|
332
|
+
```
|
333
|
+
|
334
|
+
Priority order (highest to lowest):
|
335
|
+
1. Method-level options (e.g., `sync_to_attio_now(force: true)`)
|
336
|
+
2. Model-level options (e.g., `syncs_with_attio 'people', if: :active?`)
|
337
|
+
3. Initializer configuration (e.g., `config.background_sync = true`)
|
338
|
+
4. Environment variables (e.g., `ATTIO_API_KEY`)
|
339
|
+
|
340
|
+
## Data Flow Transformations
|
341
|
+
|
342
|
+
### Transform Pipeline
|
343
|
+
|
344
|
+
```mermaid
|
345
|
+
graph LR
|
346
|
+
A[Raw Model Data] --> B[Attribute Mapping]
|
347
|
+
B --> C[Transform Function]
|
348
|
+
C --> D[Validated Data]
|
349
|
+
D --> E[API Payload]
|
350
|
+
|
351
|
+
B -.->|Example| B1[email: :work_email]
|
352
|
+
C -.->|Example| C1[Add computed fields]
|
353
|
+
D -.->|Example| D1[Remove nil values]
|
354
|
+
E -.->|Example| E1[JSON structure]
|
355
|
+
```
|
356
|
+
|
357
|
+
### Callback Chain
|
358
|
+
|
359
|
+
```mermaid
|
360
|
+
sequenceDiagram
|
361
|
+
participant Model
|
362
|
+
participant Callbacks
|
363
|
+
participant Sync
|
364
|
+
participant API
|
365
|
+
|
366
|
+
Model->>Callbacks: before_attio_sync
|
367
|
+
Callbacks->>Callbacks: prepare_data
|
368
|
+
Callbacks->>Sync: proceed with sync
|
369
|
+
Sync->>Sync: transform_attributes
|
370
|
+
Sync->>API: send data
|
371
|
+
API-->>Sync: response
|
372
|
+
Sync->>Callbacks: after_attio_sync
|
373
|
+
Callbacks->>Callbacks: log_sync
|
374
|
+
Callbacks->>Model: complete
|
375
|
+
```
|
376
|
+
|
377
|
+
## Performance Considerations
|
378
|
+
|
379
|
+
### Optimization Strategies
|
380
|
+
|
381
|
+
```mermaid
|
382
|
+
graph TD
|
383
|
+
A[Performance Optimizations] --> B[Batch Processing]
|
384
|
+
A --> C[Background Jobs]
|
385
|
+
A --> D[Connection Pooling]
|
386
|
+
A --> E[Smart Retries]
|
387
|
+
|
388
|
+
B --> F[Reduce API Calls]
|
389
|
+
C --> G[Non-blocking Operations]
|
390
|
+
D --> H[Reuse Connections]
|
391
|
+
E --> I[Exponential Backoff]
|
392
|
+
|
393
|
+
F --> J[Better Performance]
|
394
|
+
G --> J
|
395
|
+
H --> J
|
396
|
+
I --> J
|
397
|
+
```
|
398
|
+
|
399
|
+
### Load Distribution
|
400
|
+
|
401
|
+
```mermaid
|
402
|
+
graph LR
|
403
|
+
A[100 Records to Sync] --> B[BatchSync]
|
404
|
+
B --> C[Batch 1: Records 1-25]
|
405
|
+
B --> D[Batch 2: Records 26-50]
|
406
|
+
B --> E[Batch 3: Records 51-75]
|
407
|
+
B --> F[Batch 4: Records 76-100]
|
408
|
+
|
409
|
+
C --> G[Job Queue]
|
410
|
+
D --> G
|
411
|
+
E --> G
|
412
|
+
F --> G
|
413
|
+
|
414
|
+
G --> H[Worker 1]
|
415
|
+
G --> I[Worker 2]
|
416
|
+
G --> J[Worker N]
|
417
|
+
|
418
|
+
style G fill:#9cf
|
419
|
+
```
|
420
|
+
|
421
|
+
## Best Practices
|
422
|
+
|
423
|
+
### Recommended Patterns
|
424
|
+
|
425
|
+
1. **Use Background Sync for Production**
|
426
|
+
- Prevents blocking web requests
|
427
|
+
- Provides automatic retry on failure
|
428
|
+
- Better user experience
|
429
|
+
|
430
|
+
2. **Implement Error Handlers**
|
431
|
+
- Log errors to monitoring services
|
432
|
+
- Gracefully handle API downtime
|
433
|
+
- Notify administrators of issues
|
434
|
+
|
435
|
+
3. **Optimize Attribute Mapping**
|
436
|
+
- Only sync necessary fields
|
437
|
+
- Use transforms to reduce payload size
|
438
|
+
- Cache computed values when possible
|
439
|
+
|
440
|
+
4. **Test Thoroughly**
|
441
|
+
- Use provided test helpers
|
442
|
+
- Mock external API calls
|
443
|
+
- Test error scenarios
|
444
|
+
|
445
|
+
5. **Monitor Performance**
|
446
|
+
- Track sync success rates
|
447
|
+
- Monitor job queue depth
|
448
|
+
- Alert on repeated failures
|
data/Gemfile
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
source "https://rubygems.org"
|
2
4
|
|
3
5
|
# Specify your gem's dependencies in attio-rails.gemspec
|
@@ -6,15 +8,19 @@ gemspec
|
|
6
8
|
gem "rake", "~> 13.0"
|
7
9
|
|
8
10
|
group :development, :test do
|
9
|
-
gem "
|
10
|
-
gem "
|
11
|
+
gem "bundler-audit", "~> 0.9"
|
12
|
+
gem "danger", "~> 9.4"
|
13
|
+
gem "pry", "~> 0.14"
|
11
14
|
gem "rails", "~> 7.0"
|
12
|
-
gem "
|
13
|
-
gem "
|
14
|
-
gem "
|
15
|
+
gem "redcarpet", "~> 3.5"
|
16
|
+
gem "rspec", "~> 3.13"
|
17
|
+
gem "rspec-rails", "~> 6.0"
|
15
18
|
gem "rubocop", "~> 1.50"
|
16
19
|
gem "rubocop-rails", "~> 2.19"
|
17
|
-
gem "rubocop-rspec", "~>
|
18
|
-
gem "
|
19
|
-
gem "
|
20
|
+
gem "rubocop-rspec", "~> 3.6"
|
21
|
+
gem "simplecov", "~> 0.22"
|
22
|
+
gem "simplecov-console", "~> 0.9"
|
23
|
+
gem "sqlite3", "~> 1.4"
|
24
|
+
gem "webmock", "~> 3.19"
|
25
|
+
gem "yard", "~> 0.9"
|
20
26
|
end
|
data/Rakefile
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require "bundler/gem_tasks"
|
2
4
|
require "rspec/core/rake_task"
|
3
5
|
require "rubocop/rake_task"
|
@@ -7,9 +9,9 @@ RSpec::Core::RakeTask.new(:spec)
|
|
7
9
|
RuboCop::RakeTask.new
|
8
10
|
|
9
11
|
YARD::Rake::YardocTask.new do |t|
|
10
|
-
t.files = [
|
11
|
-
t.options = [
|
12
|
-
t.stats_options = [
|
12
|
+
t.files = ["lib/**/*.rb"]
|
13
|
+
t.options = ["--markup-provider=redcarpet", "--markup=markdown", "--protected", "--private"]
|
14
|
+
t.stats_options = ["--list-undoc"]
|
13
15
|
end
|
14
16
|
|
15
17
|
namespace :yard do
|
@@ -17,13 +19,13 @@ namespace :yard do
|
|
17
19
|
task :server do
|
18
20
|
sh "bundle exec yard server --reload"
|
19
21
|
end
|
20
|
-
|
22
|
+
|
21
23
|
desc "Generate documentation for GitHub Pages"
|
22
24
|
task :gh_pages do
|
23
25
|
sh "bundle exec yard --output-dir docs"
|
24
26
|
# Create .nojekyll file for GitHub Pages
|
25
|
-
File.
|
27
|
+
File.write("docs/.nojekyll", "")
|
26
28
|
end
|
27
29
|
end
|
28
30
|
|
29
|
-
task :
|
31
|
+
task default: %i[spec rubocop]
|
data/attio-rails.gemspec
CHANGED
@@ -1,4 +1,6 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "lib/attio/rails/version"
|
2
4
|
|
3
5
|
Gem::Specification.new do |spec|
|
4
6
|
spec.name = "attio-rails"
|
@@ -6,8 +8,8 @@ Gem::Specification.new do |spec|
|
|
6
8
|
spec.authors = ["Ernest Sim"]
|
7
9
|
spec.email = ["ernest.codes@gmail.com"]
|
8
10
|
|
9
|
-
spec.summary =
|
10
|
-
spec.description =
|
11
|
+
spec.summary = "Rails integration for the Attio API client"
|
12
|
+
spec.description = "Rails-specific features and integrations for the Attio Ruby client, including model concerns and generators"
|
11
13
|
spec.homepage = "https://github.com/idl3/attio-rails"
|
12
14
|
spec.license = "MIT"
|
13
15
|
spec.required_ruby_version = Gem::Requirement.new(">= 3.0.0")
|
@@ -21,26 +23,13 @@ Gem::Specification.new do |spec|
|
|
21
23
|
|
22
24
|
# Specify which files should be added to the gem when it is released.
|
23
25
|
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
24
|
-
spec.files
|
26
|
+
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
25
27
|
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
26
28
|
end
|
27
29
|
spec.bindir = "exe"
|
28
30
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
29
31
|
spec.require_paths = ["lib"]
|
30
32
|
|
31
|
-
spec.add_dependency "attio", "~> 0.1", ">= 0.1.
|
33
|
+
spec.add_dependency "attio", "~> 0.1", ">= 0.1.3"
|
32
34
|
spec.add_dependency "rails", ">= 6.1", "< 9.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.13"
|
37
|
-
spec.add_development_dependency "rubocop", "~> 1.50"
|
38
|
-
spec.add_development_dependency "simplecov", "~> 0.22"
|
39
|
-
spec.add_development_dependency "simplecov-console", "~> 0.9"
|
40
|
-
spec.add_development_dependency "webmock", "~> 3.19"
|
41
|
-
spec.add_development_dependency "pry", "~> 0.14"
|
42
|
-
spec.add_development_dependency "sqlite3", "~> 1.4"
|
43
|
-
spec.add_development_dependency "rake", "~> 13.0"
|
44
|
-
spec.add_development_dependency "bundler-audit", "~> 0.9"
|
45
|
-
spec.add_development_dependency "danger", "~> 9.4"
|
46
35
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Attio
|
2
4
|
module Rails
|
3
5
|
module Concerns
|
@@ -5,7 +7,7 @@ module Attio
|
|
5
7
|
extend ActiveSupport::Concern
|
6
8
|
|
7
9
|
included do
|
8
|
-
after_commit :sync_to_attio, on: [
|
10
|
+
after_commit :sync_to_attio, on: %i[create update], if: :should_sync_to_attio?
|
9
11
|
after_commit :remove_from_attio, on: :destroy, if: :should_remove_from_attio?
|
10
12
|
|
11
13
|
class_attribute :attio_object_type
|
@@ -18,7 +20,7 @@ module Attio
|
|
18
20
|
def syncs_with_attio(object_type, mapping = {}, options = {})
|
19
21
|
self.attio_object_type = object_type
|
20
22
|
self.attio_attribute_mapping = mapping
|
21
|
-
self.attio_sync_conditions = options[:if]
|
23
|
+
self.attio_sync_conditions = options[:if]
|
22
24
|
self.attio_identifier_attribute = options[:identifier] || :id
|
23
25
|
end
|
24
26
|
|
@@ -46,7 +48,7 @@ module Attio
|
|
46
48
|
def sync_to_attio_now
|
47
49
|
client = Attio::Rails.client
|
48
50
|
attributes = attio_attributes
|
49
|
-
|
51
|
+
|
50
52
|
if attio_record_id.present?
|
51
53
|
client.records.update(
|
52
54
|
object: attio_object_type,
|
@@ -58,7 +60,7 @@ module Attio
|
|
58
60
|
object: attio_object_type,
|
59
61
|
data: { values: attributes }
|
60
62
|
)
|
61
|
-
update_column(:attio_record_id, response[
|
63
|
+
update_column(:attio_record_id, response["data"]["id"]) if respond_to?(:attio_record_id=)
|
62
64
|
end
|
63
65
|
end
|
64
66
|
|
@@ -80,7 +82,7 @@ module Attio
|
|
80
82
|
|
81
83
|
def remove_from_attio_now
|
82
84
|
return unless attio_record_id.present?
|
83
|
-
|
85
|
+
|
84
86
|
client = Attio::Rails.client
|
85
87
|
client.records.delete(
|
86
88
|
object: attio_object_type,
|
@@ -92,23 +94,19 @@ module Attio
|
|
92
94
|
return {} unless attio_attribute_mapping
|
93
95
|
|
94
96
|
attio_attribute_mapping.each_with_object({}) do |(attio_key, local_key), hash|
|
95
|
-
value =
|
96
|
-
when Proc
|
97
|
-
local_key.call(self)
|
98
|
-
when Symbol, String
|
99
|
-
send(local_key)
|
100
|
-
else
|
101
|
-
local_key
|
102
|
-
end
|
97
|
+
value = attribute_value_for(local_key)
|
103
98
|
hash[attio_key] = value unless value.nil?
|
104
99
|
end
|
105
100
|
end
|
106
101
|
|
107
102
|
def should_sync_to_attio?
|
108
103
|
return false unless Attio::Rails.sync_enabled?
|
109
|
-
return false unless attio_object_type.present?
|
110
|
-
|
104
|
+
return false unless attio_object_type.present?
|
105
|
+
return false unless attio_attribute_mapping.present?
|
106
|
+
|
111
107
|
condition = attio_sync_conditions
|
108
|
+
return true if condition.nil?
|
109
|
+
|
112
110
|
case condition
|
113
111
|
when Proc
|
114
112
|
instance_exec(&condition)
|
@@ -126,7 +124,20 @@ module Attio
|
|
126
124
|
def attio_identifier
|
127
125
|
send(attio_identifier_attribute)
|
128
126
|
end
|
127
|
+
|
128
|
+
private def attribute_value_for(local_key)
|
129
|
+
case local_key
|
130
|
+
when Proc
|
131
|
+
local_key.call(self)
|
132
|
+
when Symbol
|
133
|
+
send(local_key)
|
134
|
+
when String
|
135
|
+
respond_to?(local_key) ? send(local_key) : local_key
|
136
|
+
else
|
137
|
+
local_key
|
138
|
+
end
|
139
|
+
end
|
129
140
|
end
|
130
141
|
end
|
131
142
|
end
|
132
|
-
end
|
143
|
+
end
|
@@ -1,12 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Attio
|
2
4
|
module Rails
|
3
5
|
class Configuration
|
4
6
|
attr_accessor :api_key, :default_workspace_id, :logger, :sync_enabled, :background_sync
|
5
7
|
|
6
8
|
def initialize
|
7
|
-
@api_key = ENV
|
9
|
+
@api_key = ENV.fetch("ATTIO_API_KEY", nil)
|
8
10
|
@default_workspace_id = nil
|
9
|
-
@logger = defined?(::Rails)
|
11
|
+
@logger = if defined?(::Rails) && ::Rails.respond_to?(:logger) && ::Rails.logger
|
12
|
+
::Rails.logger
|
13
|
+
else
|
14
|
+
::Logger.new($stdout)
|
15
|
+
end
|
10
16
|
@sync_enabled = true
|
11
17
|
@background_sync = true
|
12
18
|
end
|
@@ -30,6 +36,7 @@ module Attio
|
|
30
36
|
|
31
37
|
def client
|
32
38
|
raise ConfigurationError, "Attio API key not configured" unless configuration.valid?
|
39
|
+
|
33
40
|
@client ||= ::Attio.client(api_key: configuration.api_key)
|
34
41
|
end
|
35
42
|
|
@@ -52,4 +59,4 @@ module Attio
|
|
52
59
|
|
53
60
|
class ConfigurationError < StandardError; end
|
54
61
|
end
|
55
|
-
end
|
62
|
+
end
|