syncify 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +16 -0
- data/.rspec +3 -0
- data/.travis.yml +7 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +119 -0
- data/LICENSE.txt +21 -0
- data/README.md +196 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/syncify.rb +9 -0
- data/lib/syncify/normalize_associations.rb +40 -0
- data/lib/syncify/polymorphic_association.rb +13 -0
- data/lib/syncify/sync.rb +126 -0
- data/lib/syncify/version.rb +3 -0
- data/syncify.gemspec +39 -0
- metadata +196 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: cb790579afcec3a69a0241d30029691ede3a17ef
|
4
|
+
data.tar.gz: 71380e359bb049e5f945337e4948c2d3a6bab75e
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: d019996265b60d5a5f9c3a721e27282815766a2a3a7de3b514cccd21599d2e68b8319d6a78b78f3a31ee4a14701ea65b9db7bdc95ec8431c8a7f13ecd7e6dbdd
|
7
|
+
data.tar.gz: d8e4eaca79bae5ba9058f1205e46b8d88db5f1211262b55c35d106814f729cf69871e9856445d04f2e7b1ee22bebca94de3132851261ad35dfdf51d2ddff1616
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,119 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
syncify (0.1.0)
|
5
|
+
active_interaction (~> 3.0)
|
6
|
+
activerecord (~> 4.2)
|
7
|
+
activerecord-import (~> 0.17)
|
8
|
+
|
9
|
+
GEM
|
10
|
+
remote: https://rubygems.org/
|
11
|
+
specs:
|
12
|
+
actionpack (4.2.11.1)
|
13
|
+
actionview (= 4.2.11.1)
|
14
|
+
activesupport (= 4.2.11.1)
|
15
|
+
rack (~> 1.6)
|
16
|
+
rack-test (~> 0.6.2)
|
17
|
+
rails-dom-testing (~> 1.0, >= 1.0.5)
|
18
|
+
rails-html-sanitizer (~> 1.0, >= 1.0.2)
|
19
|
+
actionview (4.2.11.1)
|
20
|
+
activesupport (= 4.2.11.1)
|
21
|
+
builder (~> 3.1)
|
22
|
+
erubis (~> 2.7.0)
|
23
|
+
rails-dom-testing (~> 1.0, >= 1.0.5)
|
24
|
+
rails-html-sanitizer (~> 1.0, >= 1.0.3)
|
25
|
+
active_interaction (3.7.1)
|
26
|
+
activemodel (>= 4, < 7)
|
27
|
+
activemodel (4.2.11.1)
|
28
|
+
activesupport (= 4.2.11.1)
|
29
|
+
builder (~> 3.1)
|
30
|
+
activerecord (4.2.11.1)
|
31
|
+
activemodel (= 4.2.11.1)
|
32
|
+
activesupport (= 4.2.11.1)
|
33
|
+
arel (~> 6.0)
|
34
|
+
activerecord-import (0.28.2)
|
35
|
+
activerecord (>= 3.2)
|
36
|
+
activesupport (4.2.11.1)
|
37
|
+
i18n (~> 0.7)
|
38
|
+
minitest (~> 5.1)
|
39
|
+
thread_safe (~> 0.3, >= 0.3.4)
|
40
|
+
tzinfo (~> 1.1)
|
41
|
+
arel (6.0.4)
|
42
|
+
builder (3.2.3)
|
43
|
+
byebug (11.0.1)
|
44
|
+
coderay (1.1.2)
|
45
|
+
concurrent-ruby (1.1.5)
|
46
|
+
crass (1.0.4)
|
47
|
+
diff-lcs (1.3)
|
48
|
+
erubis (2.7.0)
|
49
|
+
factory_bot (4.11.1)
|
50
|
+
activesupport (>= 3.0.0)
|
51
|
+
factory_bot_rails (4.11.1)
|
52
|
+
factory_bot (~> 4.11.1)
|
53
|
+
railties (>= 3.0.0)
|
54
|
+
i18n (0.9.5)
|
55
|
+
concurrent-ruby (~> 1.0)
|
56
|
+
loofah (2.2.3)
|
57
|
+
crass (~> 1.0.2)
|
58
|
+
nokogiri (>= 1.5.9)
|
59
|
+
method_source (0.9.2)
|
60
|
+
mini_portile2 (2.4.0)
|
61
|
+
minitest (5.11.3)
|
62
|
+
nokogiri (1.10.3)
|
63
|
+
mini_portile2 (~> 2.4.0)
|
64
|
+
pry (0.12.2)
|
65
|
+
coderay (~> 1.1.0)
|
66
|
+
method_source (~> 0.9.0)
|
67
|
+
pry-byebug (3.7.0)
|
68
|
+
byebug (~> 11.0)
|
69
|
+
pry (~> 0.10)
|
70
|
+
rack (1.6.11)
|
71
|
+
rack-test (0.6.3)
|
72
|
+
rack (>= 1.0)
|
73
|
+
rails-deprecated_sanitizer (1.0.3)
|
74
|
+
activesupport (>= 4.2.0.alpha)
|
75
|
+
rails-dom-testing (1.0.9)
|
76
|
+
activesupport (>= 4.2.0, < 5.0)
|
77
|
+
nokogiri (~> 1.6)
|
78
|
+
rails-deprecated_sanitizer (>= 1.0.1)
|
79
|
+
rails-html-sanitizer (1.1.0)
|
80
|
+
loofah (~> 2.2, >= 2.2.2)
|
81
|
+
railties (4.2.11.1)
|
82
|
+
actionpack (= 4.2.11.1)
|
83
|
+
activesupport (= 4.2.11.1)
|
84
|
+
rake (>= 0.8.7)
|
85
|
+
thor (>= 0.18.1, < 2.0)
|
86
|
+
rake (10.5.0)
|
87
|
+
rspec (3.8.0)
|
88
|
+
rspec-core (~> 3.8.0)
|
89
|
+
rspec-expectations (~> 3.8.0)
|
90
|
+
rspec-mocks (~> 3.8.0)
|
91
|
+
rspec-core (3.8.2)
|
92
|
+
rspec-support (~> 3.8.0)
|
93
|
+
rspec-expectations (3.8.4)
|
94
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
95
|
+
rspec-support (~> 3.8.0)
|
96
|
+
rspec-mocks (3.8.1)
|
97
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
98
|
+
rspec-support (~> 3.8.0)
|
99
|
+
rspec-support (3.8.2)
|
100
|
+
sqlite3 (1.3.13)
|
101
|
+
thor (0.20.3)
|
102
|
+
thread_safe (0.3.6)
|
103
|
+
tzinfo (1.2.5)
|
104
|
+
thread_safe (~> 0.1)
|
105
|
+
|
106
|
+
PLATFORMS
|
107
|
+
ruby
|
108
|
+
|
109
|
+
DEPENDENCIES
|
110
|
+
bundler (~> 2.0)
|
111
|
+
factory_bot_rails (~> 4.11)
|
112
|
+
pry-byebug (~> 3.7)
|
113
|
+
rake (~> 10.0)
|
114
|
+
rspec (~> 3.0)
|
115
|
+
sqlite3 (~> 1.3, < 1.4)
|
116
|
+
syncify!
|
117
|
+
|
118
|
+
BUNDLED WITH
|
119
|
+
2.0.2
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2019 Doug Hughes
|
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,196 @@
|
|
1
|
+
# Syncify
|
2
|
+
|
3
|
+
Syncify is a gem used to sync records and associations you specify from one remote ActiveRecord environment to your local environment.
|
4
|
+
|
5
|
+
Consider this hypothetical problem: You have a gigantic production database with complex associations between your models, including polymorphic associations. The database includes sensitive data that shouldn't really be in your development or staging environments. But, there's something wrong in production and you need production data to be able to debug it. It's not practical, efficient, safe, or generally advisable to restore a backup of the production database locally.
|
6
|
+
|
7
|
+
How do you get that data safely from production to an environment where you can make use of it? This is the problem that Syncify aims to address.
|
8
|
+
|
9
|
+
## Installation
|
10
|
+
|
11
|
+
Add this line to your application's Gemfile:
|
12
|
+
|
13
|
+
```ruby
|
14
|
+
gem 'syncify'
|
15
|
+
```
|
16
|
+
|
17
|
+
And then execute:
|
18
|
+
|
19
|
+
$ bundle
|
20
|
+
|
21
|
+
Or install it yourself as:
|
22
|
+
|
23
|
+
$ gem install syncify
|
24
|
+
|
25
|
+
## Usage
|
26
|
+
|
27
|
+
|
28
|
+
Syncify doesn't require Rails, just ActiveRecord, but it's a reasonable foundation for the following examples.
|
29
|
+
|
30
|
+
Also, you can sync from any environment to whatever your current environment is. So, you could sync from your staging environment to your client test environment or from staging to development. Heck, you could go from staging to prod if you'd like.
|
31
|
+
|
32
|
+
For the purposes of this documentation we'll assume that you're syncing data in a Rails app from production to your local development environment.
|
33
|
+
|
34
|
+
Syncify has a pretty simple API. There's just one method, `run!`. Here's a really basic example where we're syncing a single `Widget` from production to the current environment. The current environment could be any environment, but we'll assume it's development for this documentation.
|
35
|
+
|
36
|
+
```ruby
|
37
|
+
Syncify::Sync.run!(klass: Widget, id: 123, remote_database: :production)
|
38
|
+
```
|
39
|
+
|
40
|
+
Boom! You've copied `Widget` 123 to local dev. The widget will have all the same values including its `id`, `created_at`, `updated_at`, etc values. The above example assumes that the `Widget` doesn't have any foreign keys that don't exist locally.
|
41
|
+
|
42
|
+
Now, let's say a `Widget` `belongs_to` a `Manufacturer`. Furthermore, let's say a `Manufacturer` `has_many` `Widget`s. We'll pretend we have this data in the prod database:
|
43
|
+
|
44
|
+
`widgets`:
|
45
|
+
|
46
|
+
| id | name | manufacturer_id |
|
47
|
+
| --- | -------------------------------------------------- | --------------- |
|
48
|
+
| 123 | Lubricated Stainless Steel Helical Insert | 78 |
|
49
|
+
| 124 | Magnetic Contact Alarm Switches | 79 |
|
50
|
+
| 125 | Idler Sprocket for Double-Strand ANSI Roller Chain | 78 |
|
51
|
+
| 126 | Rod End Bolt Blank | 79 |
|
52
|
+
| 127 | Press-Fit Drill Bushing | 78 |
|
53
|
+
|
54
|
+
`manufacturers`:
|
55
|
+
|
56
|
+
| id | name |
|
57
|
+
| --- | -------------------------- |
|
58
|
+
| 78 | South Seas Trading Company |
|
59
|
+
| 79 | Blandco Manufacturing |
|
60
|
+
|
61
|
+
If your database uses foreign keys and the production `Widget`'s `Manufacturer` doesn't exist locally then the example above would fail. To get around this we can specify an association to also sync when syncing the `Widget`.
|
62
|
+
|
63
|
+
```ruby
|
64
|
+
Syncify::Sync.run!(klass: Widget,
|
65
|
+
id: 123,
|
66
|
+
association: :manufacturer,
|
67
|
+
remote_database : :production)
|
68
|
+
```
|
69
|
+
|
70
|
+
Running the above example will copy two records into your local database:
|
71
|
+
|
72
|
+
* The `Widget` with id 123 (Lubricated Stainless Steel Helical Insert)
|
73
|
+
* The `Manufacturer` with id 78 (South Seas Trading Company)
|
74
|
+
|
75
|
+
It's important to note that Syncer _does not_ recursively follow associations. You'll note that not all of the the manufacturer's widgets were synced, only the one we specified.
|
76
|
+
|
77
|
+
The `association` attribute passed into the `run!` method can be any valid value that you might use when joining records with ActiveRecord. The above effectively becomes:
|
78
|
+
|
79
|
+
```ruby
|
80
|
+
Widget.eager_load(:manufacturer).find(123)
|
81
|
+
```
|
82
|
+
|
83
|
+
Because of this, you can pass any sort of ActiveRecord association into Syncify's `run!` method.
|
84
|
+
|
85
|
+
Now let's imagine that a `Manufacturer` `has_many` `Plant`s which `belong_to` a `State`. Here are some example rows in these tables:
|
86
|
+
|
87
|
+
`plants`:
|
88
|
+
|
89
|
+
| id | name | manufacturer_id | city | state_id |
|
90
|
+
| --- | --------------- | --------------- | ------- | -------- |
|
91
|
+
| 64 | Rapid Run | 78 | Lansing | 13 |
|
92
|
+
| 65 | Ye Olde Factory | 78 | Naples | 15 |
|
93
|
+
| 66 | Catco | 79 | Balston | 43 |
|
94
|
+
|
95
|
+
`states`:
|
96
|
+
|
97
|
+
| id | name |
|
98
|
+
| --- | -------- |
|
99
|
+
| 13 | Michigan |
|
100
|
+
| 15 | Florida |
|
101
|
+
| 43 | Virginia |
|
102
|
+
|
103
|
+
We could sync a `Manufacturer` along with its widgets, factories, and the state the factory is in with this example:
|
104
|
+
|
105
|
+
```ruby
|
106
|
+
Syncify::Sync.run!(klass: Manufacturer,
|
107
|
+
id: 78,
|
108
|
+
association: [
|
109
|
+
:widgets,
|
110
|
+
{ factories: :state }
|
111
|
+
],
|
112
|
+
remote_database: :production)
|
113
|
+
```
|
114
|
+
|
115
|
+
You can really go wild with the associations; well beyond what you could normally run with an ActiveRecord query! In one app I have a hash defining a ton of associations across dozens of models that is more than 150 lines long. When I run this sync it identifies more than 500 records and syncs them all to local dev in about 30 seconds.
|
116
|
+
|
117
|
+
### Polymorphic Associations
|
118
|
+
|
119
|
+
Syncify also works with (and across) Polymorphic associations! To sync across polymorphic associations you need to specify an association using the `Syncer::PolymorphicAssociation` class. This is put in place in your otherwise-normal associations.
|
120
|
+
|
121
|
+
Let's imagine that we run an online store that sells both physical and digital goods. A given invoice then might have line items that refer to either type of good.
|
122
|
+
|
123
|
+
Here's our model:
|
124
|
+
|
125
|
+
* `Customer`
|
126
|
+
* `has_many :invoices`
|
127
|
+
* `Invoice`
|
128
|
+
* `belongs_to :customer`
|
129
|
+
* `has_many :line_items`
|
130
|
+
* `LineItem`
|
131
|
+
* `belongs_to :invoice`
|
132
|
+
* `belongs_to :product, polymorphic: true`
|
133
|
+
* `DigitalProduct`
|
134
|
+
* `has_many :line_items, as: :product`
|
135
|
+
* `belongs_to :category`
|
136
|
+
* `PhysicalProduct`
|
137
|
+
* `has_many :line_items, as: :product`
|
138
|
+
* `belongs_to :distributor`
|
139
|
+
* `Category`
|
140
|
+
* `has_many :digital_products`
|
141
|
+
* `Distributor`
|
142
|
+
* `has_many :physical_products`
|
143
|
+
|
144
|
+
There's a lot going on above, and I'll spare you the example database tables. You can use your imagination! 😉
|
145
|
+
|
146
|
+
Let's say we want to sync a particular `LineItem`. With ActiveRecord queries you can't simply `eager_load` across a polymorphic association, much less to any sub-associations (EG: category or distributor). With Syncify you can.
|
147
|
+
|
148
|
+
Here's an example. For simplicity's sake it assumes that the database doesn't use foreign keys. (Don't worry, we'll do a more complex example next!):
|
149
|
+
|
150
|
+
```ruby
|
151
|
+
Syncify::Sync.run!(klass: Customer,
|
152
|
+
id: 999,
|
153
|
+
association: Syncer::PolymorphicAssociation.new(
|
154
|
+
:product,
|
155
|
+
DigitalProduct => {},
|
156
|
+
PhysicalProduct => {}
|
157
|
+
),
|
158
|
+
remote_database: :production)
|
159
|
+
```
|
160
|
+
|
161
|
+
Assuming that line item 42's product is a `DigitalProduct`, this example would have synced the `LineItem` and its `DigitalProduct` and nothing else.
|
162
|
+
|
163
|
+
The `Syncer::PolymorphicAssociation` is saying that, for the `LineItem`'s `product` polymorphic association, when the product is a `DigitalProduct`, sync it with the specified associations (in this case none). When the product is a `PhysicalProduct`, sync it with the specified associations (again, none in this case).
|
164
|
+
|
165
|
+
Now let's say that we want to sync a specific `Customer` and all of their invoices and the related products. IE: the whole kit and caboodle. Here's how you can do it:
|
166
|
+
|
167
|
+
```ruby
|
168
|
+
Syncify::Sync.run!(klass: Customer,
|
169
|
+
id: 999,
|
170
|
+
association: {
|
171
|
+
invoices: {
|
172
|
+
line_items: Syncer::PolymorphicAssociation.new(
|
173
|
+
:product,
|
174
|
+
DigitalProduct => :category,
|
175
|
+
PhysicalProduct => :distributor
|
176
|
+
)
|
177
|
+
}
|
178
|
+
},
|
179
|
+
remote_database: :production)
|
180
|
+
```
|
181
|
+
|
182
|
+
This will sync a customer, all of their invoices, all of those invoice's line items. It goes on to sync all of the line item's products, whether digital or physical, as well as the digital product's category and the physical product's distributor.
|
183
|
+
|
184
|
+
## Development
|
185
|
+
|
186
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
187
|
+
|
188
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
189
|
+
|
190
|
+
## Contributing
|
191
|
+
|
192
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/dhughes/syncify.
|
193
|
+
|
194
|
+
## License
|
195
|
+
|
196
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "syncify"
|
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
data/lib/syncify.rb
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Syncify
|
4
|
+
class NormalizeAssociations < ActiveInteraction::Base
|
5
|
+
object :association, class: Object
|
6
|
+
|
7
|
+
def execute
|
8
|
+
normalize_associations(association)
|
9
|
+
end
|
10
|
+
|
11
|
+
private
|
12
|
+
|
13
|
+
def normalize_associations(association)
|
14
|
+
Array.wrap(
|
15
|
+
case association
|
16
|
+
when Symbol
|
17
|
+
Hash[association, {}]
|
18
|
+
when Array
|
19
|
+
association.map { |node| normalize_associations(node) }
|
20
|
+
when Hash
|
21
|
+
association.reduce([]) do |memo, (key, value)|
|
22
|
+
values = normalize_associations(value)
|
23
|
+
|
24
|
+
if values.empty?
|
25
|
+
memo << Hash[key, {}]
|
26
|
+
else
|
27
|
+
values.each do |value|
|
28
|
+
memo << Hash[key, value]
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
memo
|
33
|
+
end
|
34
|
+
else
|
35
|
+
association
|
36
|
+
end
|
37
|
+
).flatten
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
data/lib/syncify/sync.rb
ADDED
@@ -0,0 +1,126 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Syncify
|
4
|
+
class Sync < ActiveInteraction::Base
|
5
|
+
object :klass, class: Class
|
6
|
+
integer :id
|
7
|
+
object :association, class: Object, default: []
|
8
|
+
object :callback, class: Proc, default: nil
|
9
|
+
|
10
|
+
symbol :remote_database
|
11
|
+
|
12
|
+
attr_accessor :identified_records
|
13
|
+
|
14
|
+
def execute
|
15
|
+
@identified_records = Set[]
|
16
|
+
|
17
|
+
remote do
|
18
|
+
identify_associated_records(klass.find(id), normalized_associations(association))
|
19
|
+
end
|
20
|
+
|
21
|
+
callback.call(identified_records) if callback.present?
|
22
|
+
|
23
|
+
sync_records
|
24
|
+
end
|
25
|
+
|
26
|
+
def identify_associated_records(root, associations)
|
27
|
+
identified_records << root
|
28
|
+
|
29
|
+
standard_associations = associations.reject(&method(:includes_polymorphic_association))
|
30
|
+
polymorphic_associations = associations.select(&method(:includes_polymorphic_association))
|
31
|
+
|
32
|
+
standard_associations.each do |association|
|
33
|
+
traverse_associations(
|
34
|
+
root.class.eager_load(association).find(root.id),
|
35
|
+
association
|
36
|
+
)
|
37
|
+
end
|
38
|
+
|
39
|
+
identify_polymorphic_associated_records(root, polymorphic_associations)
|
40
|
+
end
|
41
|
+
|
42
|
+
def identify_polymorphic_associated_records(root, polymorphic_associations)
|
43
|
+
polymorphic_associations.each do |polymorphic_association|
|
44
|
+
if polymorphic_association.is_a?(Hash)
|
45
|
+
polymorphic_association.each do |key, association|
|
46
|
+
Array.wrap(root.__send__(key)).each do |target|
|
47
|
+
identify_polymorphic_associated_records(target, Array.wrap(association))
|
48
|
+
end
|
49
|
+
end
|
50
|
+
else
|
51
|
+
target = root.__send__(polymorphic_association.property)
|
52
|
+
type = polymorphic_association.associations.keys.detect do |association_type|
|
53
|
+
target.is_a?(association_type)
|
54
|
+
end
|
55
|
+
associations = polymorphic_association.associations[type]
|
56
|
+
identify_associated_records(target, normalized_associations(associations))
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def traverse_associations(records, associations)
|
62
|
+
records = Array(records)
|
63
|
+
|
64
|
+
identified_records.merge records
|
65
|
+
|
66
|
+
records.each do |record|
|
67
|
+
associations.each do |association, nested_associations|
|
68
|
+
traverse_associations(record.__send__(association), nested_associations)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def sync_records
|
74
|
+
ActiveRecord::Base.connection.disable_referential_integrity do
|
75
|
+
classify_identified_instances.each do |class_name, new_instances|
|
76
|
+
puts "Syncing #{new_instances.size} #{class_name} objects"
|
77
|
+
clazz = Object.const_get(class_name)
|
78
|
+
clazz.import(new_instances, validate: false, on_duplicate_key_update: [:id])
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def classify_identified_instances
|
84
|
+
puts "Classifying #{identified_records.size} records for bulk import."
|
85
|
+
|
86
|
+
identified_records.each_with_object({}) do |instance, memo|
|
87
|
+
clazz = instance.class
|
88
|
+
class_name = clazz.name
|
89
|
+
memo[class_name] ||= []
|
90
|
+
memo[class_name] << clone_instance(instance)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def clone_instance(instance)
|
95
|
+
clazz = instance.class
|
96
|
+
new_instance = clazz.new
|
97
|
+
|
98
|
+
instance.attributes.each do |attribute, value|
|
99
|
+
new_instance[attribute.to_s] = value
|
100
|
+
end
|
101
|
+
|
102
|
+
new_instance
|
103
|
+
end
|
104
|
+
|
105
|
+
def includes_polymorphic_association(association)
|
106
|
+
association.to_s.include?('Syncify::PolymorphicAssociation')
|
107
|
+
end
|
108
|
+
|
109
|
+
def normalized_associations(association)
|
110
|
+
Syncify::NormalizeAssociations.run!(association: association)
|
111
|
+
end
|
112
|
+
|
113
|
+
def remote
|
114
|
+
run_in_environment(remote_database) { yield }
|
115
|
+
end
|
116
|
+
|
117
|
+
def run_in_environment(environment)
|
118
|
+
initial_config = ActiveRecord::Base.connection_config
|
119
|
+
ActiveRecord::Base.establish_connection environment
|
120
|
+
yield
|
121
|
+
ensure
|
122
|
+
ActiveRecord::Base.establish_connection initial_config
|
123
|
+
end
|
124
|
+
|
125
|
+
end
|
126
|
+
end
|
data/syncify.gemspec
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
lib = File.expand_path("lib", __dir__)
|
2
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
3
|
+
require "syncify/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = "syncify"
|
7
|
+
spec.version = Syncify::VERSION
|
8
|
+
spec.authors = ["Doug Hughes"]
|
9
|
+
spec.email = ["doug@doughughes.net"]
|
10
|
+
|
11
|
+
spec.summary = %q{Copies data between Rails environments}
|
12
|
+
spec.description = %q{You can use this gem to copy records and their specified associations from production (or other) environments to your local environment.}
|
13
|
+
spec.homepage = "http://github.com/dhughes/syncify"
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
17
|
+
spec.metadata["source_code_uri"] = "http://github.com/dhughes/syncify"
|
18
|
+
spec.metadata["changelog_uri"] = "http://github.com/dhughes/syncify"
|
19
|
+
|
20
|
+
# Specify which files should be added to the gem when it is released.
|
21
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
22
|
+
spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
|
23
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
24
|
+
end
|
25
|
+
spec.bindir = "exe"
|
26
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
27
|
+
spec.require_paths = ["lib"]
|
28
|
+
|
29
|
+
spec.add_development_dependency "bundler", "~> 2.0"
|
30
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
31
|
+
spec.add_development_dependency "rspec", "~> 3.0"
|
32
|
+
spec.add_development_dependency "pry-byebug", "~> 3.7"
|
33
|
+
spec.add_development_dependency "sqlite3", '~> 1.3', '< 1.4'
|
34
|
+
spec.add_development_dependency "factory_bot_rails", "~> 4.11"
|
35
|
+
|
36
|
+
spec.add_runtime_dependency "active_interaction", "~> 3.0"
|
37
|
+
spec.add_runtime_dependency "activerecord", "~> 4.2"
|
38
|
+
spec.add_runtime_dependency "activerecord-import", "~> 0.17"
|
39
|
+
end
|
metadata
ADDED
@@ -0,0 +1,196 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: syncify
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Doug Hughes
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2019-08-08 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '2.0'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '2.0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '10.0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '10.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rspec
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '3.0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '3.0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: pry-byebug
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '3.7'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '3.7'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: sqlite3
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '1.3'
|
76
|
+
- - "<"
|
77
|
+
- !ruby/object:Gem::Version
|
78
|
+
version: '1.4'
|
79
|
+
type: :development
|
80
|
+
prerelease: false
|
81
|
+
version_requirements: !ruby/object:Gem::Requirement
|
82
|
+
requirements:
|
83
|
+
- - "~>"
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
version: '1.3'
|
86
|
+
- - "<"
|
87
|
+
- !ruby/object:Gem::Version
|
88
|
+
version: '1.4'
|
89
|
+
- !ruby/object:Gem::Dependency
|
90
|
+
name: factory_bot_rails
|
91
|
+
requirement: !ruby/object:Gem::Requirement
|
92
|
+
requirements:
|
93
|
+
- - "~>"
|
94
|
+
- !ruby/object:Gem::Version
|
95
|
+
version: '4.11'
|
96
|
+
type: :development
|
97
|
+
prerelease: false
|
98
|
+
version_requirements: !ruby/object:Gem::Requirement
|
99
|
+
requirements:
|
100
|
+
- - "~>"
|
101
|
+
- !ruby/object:Gem::Version
|
102
|
+
version: '4.11'
|
103
|
+
- !ruby/object:Gem::Dependency
|
104
|
+
name: active_interaction
|
105
|
+
requirement: !ruby/object:Gem::Requirement
|
106
|
+
requirements:
|
107
|
+
- - "~>"
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: '3.0'
|
110
|
+
type: :runtime
|
111
|
+
prerelease: false
|
112
|
+
version_requirements: !ruby/object:Gem::Requirement
|
113
|
+
requirements:
|
114
|
+
- - "~>"
|
115
|
+
- !ruby/object:Gem::Version
|
116
|
+
version: '3.0'
|
117
|
+
- !ruby/object:Gem::Dependency
|
118
|
+
name: activerecord
|
119
|
+
requirement: !ruby/object:Gem::Requirement
|
120
|
+
requirements:
|
121
|
+
- - "~>"
|
122
|
+
- !ruby/object:Gem::Version
|
123
|
+
version: '4.2'
|
124
|
+
type: :runtime
|
125
|
+
prerelease: false
|
126
|
+
version_requirements: !ruby/object:Gem::Requirement
|
127
|
+
requirements:
|
128
|
+
- - "~>"
|
129
|
+
- !ruby/object:Gem::Version
|
130
|
+
version: '4.2'
|
131
|
+
- !ruby/object:Gem::Dependency
|
132
|
+
name: activerecord-import
|
133
|
+
requirement: !ruby/object:Gem::Requirement
|
134
|
+
requirements:
|
135
|
+
- - "~>"
|
136
|
+
- !ruby/object:Gem::Version
|
137
|
+
version: '0.17'
|
138
|
+
type: :runtime
|
139
|
+
prerelease: false
|
140
|
+
version_requirements: !ruby/object:Gem::Requirement
|
141
|
+
requirements:
|
142
|
+
- - "~>"
|
143
|
+
- !ruby/object:Gem::Version
|
144
|
+
version: '0.17'
|
145
|
+
description: You can use this gem to copy records and their specified associations
|
146
|
+
from production (or other) environments to your local environment.
|
147
|
+
email:
|
148
|
+
- doug@doughughes.net
|
149
|
+
executables: []
|
150
|
+
extensions: []
|
151
|
+
extra_rdoc_files: []
|
152
|
+
files:
|
153
|
+
- ".gitignore"
|
154
|
+
- ".rspec"
|
155
|
+
- ".travis.yml"
|
156
|
+
- Gemfile
|
157
|
+
- Gemfile.lock
|
158
|
+
- LICENSE.txt
|
159
|
+
- README.md
|
160
|
+
- Rakefile
|
161
|
+
- bin/console
|
162
|
+
- bin/setup
|
163
|
+
- lib/syncify.rb
|
164
|
+
- lib/syncify/normalize_associations.rb
|
165
|
+
- lib/syncify/polymorphic_association.rb
|
166
|
+
- lib/syncify/sync.rb
|
167
|
+
- lib/syncify/version.rb
|
168
|
+
- syncify.gemspec
|
169
|
+
homepage: http://github.com/dhughes/syncify
|
170
|
+
licenses:
|
171
|
+
- MIT
|
172
|
+
metadata:
|
173
|
+
homepage_uri: http://github.com/dhughes/syncify
|
174
|
+
source_code_uri: http://github.com/dhughes/syncify
|
175
|
+
changelog_uri: http://github.com/dhughes/syncify
|
176
|
+
post_install_message:
|
177
|
+
rdoc_options: []
|
178
|
+
require_paths:
|
179
|
+
- lib
|
180
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
181
|
+
requirements:
|
182
|
+
- - ">="
|
183
|
+
- !ruby/object:Gem::Version
|
184
|
+
version: '0'
|
185
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
186
|
+
requirements:
|
187
|
+
- - ">="
|
188
|
+
- !ruby/object:Gem::Version
|
189
|
+
version: '0'
|
190
|
+
requirements: []
|
191
|
+
rubyforge_project:
|
192
|
+
rubygems_version: 2.5.2.3
|
193
|
+
signing_key:
|
194
|
+
specification_version: 4
|
195
|
+
summary: Copies data between Rails environments
|
196
|
+
test_files: []
|