seed_dumpling 1.0.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/LICENSE.txt +21 -0
- data/README.md +127 -0
- data/lib/seed_dumpling/dump_methods/enumeration.rb +63 -0
- data/lib/seed_dumpling/dump_methods.rb +226 -0
- data/lib/seed_dumpling/environment.rb +152 -0
- data/lib/seed_dumpling/railtie.rb +10 -0
- data/lib/seed_dumpling/version.rb +5 -0
- data/lib/seed_dumpling.rb +15 -0
- data/lib/tasks/seed_dumpling.rake +10 -0
- metadata +87 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 34ece3f93f37d8317c2a9d7ea58dfcd05eb4c041c5bb39a8a0e835b15eb9d9ec
|
4
|
+
data.tar.gz: 8ebb42ccb0329c74629bc8b3bc536da1430a360a97b9b598ea8b8425c794018f
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: b968a70bcb038cd358d14f467cd98961c5dcad03d2452470d45ed9e878c8732dc3334b2cdfdffae4e0ea9057c32522162e75eb8b89eb5d55312ff15338600eee
|
7
|
+
data.tar.gz: 4573204701121d23e60047e80464b6b4674932e32e020184ee6ecba3998dc940fa8cd5ccb97e408f506757268f8f499d49c054c6f4a94f7b185e3574e706e5fb
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2024 Mikael Henriksson
|
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,127 @@
|
|
1
|
+
Seed Dump
|
2
|
+
========
|
3
|
+
|
4
|
+
Seed Dump is a Rails 7+ that adds a rake task named `db:seed:dump`.
|
5
|
+
It allows you to create seed data files from the existing data in your database.
|
6
|
+
You can also use Seed Dump from the Rails console. See below for usage examples.
|
7
|
+
|
8
|
+
Installation
|
9
|
+
------------
|
10
|
+
|
11
|
+
Add it to your Gemfile with:
|
12
|
+
```ruby
|
13
|
+
gem 'seed_dumpling'
|
14
|
+
```
|
15
|
+
Or install it by hand:
|
16
|
+
```sh
|
17
|
+
gem install seed_dumpling
|
18
|
+
```
|
19
|
+
Examples
|
20
|
+
--------
|
21
|
+
|
22
|
+
### Rake task
|
23
|
+
|
24
|
+
Dump all data directly to `db/seeds.rb`:
|
25
|
+
```sh
|
26
|
+
rake db:seed:dump
|
27
|
+
```
|
28
|
+
Result:
|
29
|
+
```ruby
|
30
|
+
Product.create!([
|
31
|
+
{ category_id: 1, description: "Long Sleeve Shirt", name: "Long Sleeve Shirt" },
|
32
|
+
{ category_id: 3, description: "Plain White Tee Shirt", name: "Plain T-Shirt" }
|
33
|
+
])
|
34
|
+
User.create!([
|
35
|
+
{ password: "123456", username: "test_1" },
|
36
|
+
{ password: "234567", username: "test_2" }
|
37
|
+
])
|
38
|
+
```
|
39
|
+
|
40
|
+
Dump only data from the users table and dump a maximum of 1 record:
|
41
|
+
```sh
|
42
|
+
$ rake db:seed:dump MODELS=User LIMIT=1
|
43
|
+
```
|
44
|
+
|
45
|
+
Result:
|
46
|
+
```ruby
|
47
|
+
User.create!([
|
48
|
+
{ password: "123456", username: "test_1" }
|
49
|
+
])
|
50
|
+
```
|
51
|
+
|
52
|
+
Append to `db/seeds.rb` instead of overwriting it:
|
53
|
+
```sh
|
54
|
+
rake db:seed:dump APPEND=true
|
55
|
+
```
|
56
|
+
|
57
|
+
Use another output file instead of `db/seeds.rb`:
|
58
|
+
```sh
|
59
|
+
rake db:seed:dump FILE=db/seeds/users.rb
|
60
|
+
```
|
61
|
+
|
62
|
+
Exclude `name` and `age` from the dump:
|
63
|
+
```sh
|
64
|
+
rake db:seed:dump EXCLUDE=name,age
|
65
|
+
```
|
66
|
+
|
67
|
+
There are more options that can be set— see below for all of them.
|
68
|
+
|
69
|
+
### Console
|
70
|
+
|
71
|
+
Output a dump of all User records:
|
72
|
+
```ruby
|
73
|
+
irb(main):001:0> puts SeedDumpling.dump(User)
|
74
|
+
User.create!([
|
75
|
+
{ password: "123456", username: "test_1" },
|
76
|
+
{ password: "234567", username: "test_2" }
|
77
|
+
])
|
78
|
+
```
|
79
|
+
|
80
|
+
Write the dump to a file:
|
81
|
+
```ruby
|
82
|
+
irb(main):002:0> SeedDumpling.dump(User, file: 'db/seeds.rb')
|
83
|
+
```
|
84
|
+
|
85
|
+
Append the dump to a file:
|
86
|
+
```ruby
|
87
|
+
irb(main):003:0> SeedDumpling.dump(User, file: 'db/seeds.rb', append: true)
|
88
|
+
```
|
89
|
+
|
90
|
+
Exclude `name` and `age` from the dump:
|
91
|
+
```ruby
|
92
|
+
irb(main):004:0> SeedDumpling.dump(User, exclude: [:name, :age])
|
93
|
+
```
|
94
|
+
|
95
|
+
Options are specified as a Hash for the second argument.
|
96
|
+
|
97
|
+
In the console, any relation of ActiveRecord rows can be dumped (not individual objects though)
|
98
|
+
```ruby
|
99
|
+
irb(main):001:0> puts SeedDumpling.dump(User.where(is_admin: false))
|
100
|
+
User.create!([
|
101
|
+
{ password: "123456", username: "test_1", is_admin: false },
|
102
|
+
{ password: "234567", username: "test_2", is_admin: false }
|
103
|
+
])
|
104
|
+
```
|
105
|
+
|
106
|
+
Options
|
107
|
+
-------
|
108
|
+
|
109
|
+
Options are common to both the Rake task and the console, except where noted.
|
110
|
+
|
111
|
+
`append`: If set to `true`, append the data to the file instead of overwriting it. Default: `false`.
|
112
|
+
|
113
|
+
`batch_size`: Controls the number of records that are written to file at a given time. Default: 1000. If you're running out of memory when dumping, try decreasing this. If things are dumping too slow, trying increasing this.
|
114
|
+
|
115
|
+
`exclude`: Attributes to be excluded from the dump. Pass a comma-separated list to the Rake task (i.e. `name,age`) and an array on the console (i.e. `[:name, :age]`). Default: `[:id, :created_at, :updated_at]`.
|
116
|
+
|
117
|
+
`file`: Write to the specified output file. The Rake task default is `db/seeds.rb`. The console returns the dump as a string by default.
|
118
|
+
|
119
|
+
`import`: If `true`, output will be in the format needed by the [activerecord-import](https://github.com/zdennis/activerecord-import) gem, rather than the default format. Default: `false`.
|
120
|
+
|
121
|
+
`limit`: Dump no more than this amount of data. Default: no limit. Rake task only. In the console just pass in an ActiveRecord::Relation with the appropriate limit (e.g. `SeedDumpling.dump(User.limit(5))`).
|
122
|
+
|
123
|
+
`conditions`: Dump only specific records. In the console just pass in an ActiveRecord::Relation with the appropriate conditions (e.g. `SeedDumpling.dump(User.where(state: :active))`).
|
124
|
+
|
125
|
+
`model[s]`: Restrict the dump to the specified comma-separated list of models. Default: all models. If you are using a Rails engine you can dump a specific model by passing "EngineName::ModelName". Rake task only. Example: `rake db:seed:dump MODELS="User, Position, Function"`
|
126
|
+
|
127
|
+
`models_exclude`: Exclude the specified comma-separated list of models from the dump. Default: no models excluded. Rake task only. Example: `rake db:seed:dump MODELS_EXCLUDE="User"`
|
@@ -0,0 +1,63 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class SeedDumpling
|
4
|
+
module DumpMethods
|
5
|
+
#
|
6
|
+
# Helpermethods for enumerating active records
|
7
|
+
#
|
8
|
+
module Enumeration
|
9
|
+
def active_record_enumeration(records, options) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
10
|
+
# Ensure records are ordered by primary key if not already ordered
|
11
|
+
unless records.respond_to?(:arel) && records.arel.orders.present?
|
12
|
+
records = records.order("#{records.quoted_table_name}.#{records.quoted_primary_key} ASC")
|
13
|
+
end
|
14
|
+
|
15
|
+
num_of_batches, batch_size, last_batch_size = batch_params_from(records, options)
|
16
|
+
|
17
|
+
# Iterate over each batch
|
18
|
+
(1..num_of_batches).each do |batch_number|
|
19
|
+
last_batch = (batch_number == num_of_batches)
|
20
|
+
current_batch_size = last_batch ? last_batch_size : batch_size
|
21
|
+
|
22
|
+
# Fetch and process records for the current batch
|
23
|
+
record_strings = records
|
24
|
+
.offset((batch_number - 1) * batch_size)
|
25
|
+
.limit(current_batch_size)
|
26
|
+
.map { |record| dump_record(record, options) }
|
27
|
+
|
28
|
+
yield record_strings, last_batch
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def enumerable_enumeration(records, options)
|
33
|
+
_, batch_size = batch_params_from(records, options)
|
34
|
+
record_strings = []
|
35
|
+
|
36
|
+
records.each_with_index do |record, index|
|
37
|
+
record_strings << dump_record(record, options)
|
38
|
+
last_batch = (index == records.size - 1)
|
39
|
+
|
40
|
+
if record_strings.size == batch_size || last_batch
|
41
|
+
yield record_strings, last_batch
|
42
|
+
record_strings = []
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def batch_params_from(records, options)
|
48
|
+
batch_size = batch_size_from(options)
|
49
|
+
total_count = records.count
|
50
|
+
|
51
|
+
num_of_batches = (total_count.to_f / batch_size).ceil
|
52
|
+
remainder = total_count % batch_size
|
53
|
+
last_batch_size = remainder.zero? ? batch_size : remainder
|
54
|
+
|
55
|
+
[num_of_batches, batch_size, last_batch_size]
|
56
|
+
end
|
57
|
+
|
58
|
+
def batch_size_from(options)
|
59
|
+
options[:batch_size]&.to_i || 1000
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,226 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "action_text"
|
4
|
+
require "active_storage"
|
5
|
+
|
6
|
+
class SeedDumpling
|
7
|
+
# Provides methods for dumping database records into Ruby code that can be used
|
8
|
+
# as seeds. Handles various data types and supports both create and import formats.
|
9
|
+
module DumpMethods # rubocop:disable Metrics/ModuleLength
|
10
|
+
extend ActiveSupport::Concern
|
11
|
+
|
12
|
+
include Enumeration
|
13
|
+
|
14
|
+
class_methods do
|
15
|
+
def dump(...)
|
16
|
+
new.dump(...)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def dump(records, options = {})
|
21
|
+
return nil if records.none?
|
22
|
+
|
23
|
+
io = prepare_io(options)
|
24
|
+
write_records(records, io, options)
|
25
|
+
ensure
|
26
|
+
io&.close
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def dump_record(record, options) # rubocop:disable Metrics/AbcSize
|
32
|
+
attributes = filter_attributes(record.attributes, options[:exclude])
|
33
|
+
|
34
|
+
# Make sure we include attachments and rich text in the final output
|
35
|
+
attributes["avatar"] = record.avatar if record.respond_to?(:avatar) && defined?(ActiveStorage::Attached::One)
|
36
|
+
|
37
|
+
attributes["photos"] = record.photos if record.respond_to?(:photos) && defined?(ActiveStorage::Attached::Many)
|
38
|
+
|
39
|
+
attributes["content"] = record.content if record.respond_to?(:content) && record.content.is_a?(ActionText::RichText)
|
40
|
+
|
41
|
+
formatted_attributes = format_attributes(attributes, options[:import])
|
42
|
+
wrap_attributes(formatted_attributes, options[:import])
|
43
|
+
end
|
44
|
+
|
45
|
+
def filter_attributes(attributes, exclude)
|
46
|
+
attributes.select do |key, _|
|
47
|
+
(key.is_a?(String) || key.is_a?(Symbol)) && exclude.exclude?(key.to_sym)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def format_attributes(attributes, import_format)
|
52
|
+
attributes.map do |attribute, value|
|
53
|
+
import_format ? value_to_s(value) : "#{attribute}: #{value_to_s(value)}"
|
54
|
+
end.join(", ")
|
55
|
+
end
|
56
|
+
|
57
|
+
def wrap_attributes(attribute_string, import_format)
|
58
|
+
open_char, close_char = import_format ? ["[", "]"] : ["{", "}"]
|
59
|
+
"#{open_char}#{attribute_string}#{close_char}"
|
60
|
+
end
|
61
|
+
|
62
|
+
def value_to_s(value) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity
|
63
|
+
formatted_value = case value
|
64
|
+
when BigDecimal, IPAddr, ->(v) { rgeo_instance?(v) }
|
65
|
+
value.to_s
|
66
|
+
when Date, Time, DateTime
|
67
|
+
value.to_fs(:db)
|
68
|
+
when Range
|
69
|
+
range_to_string(value)
|
70
|
+
when ->(v) { defined?(ActiveStorage::Attached::One) && v.is_a?(ActiveStorage::Attached::One) }
|
71
|
+
handle_active_storage_one(value)
|
72
|
+
when ->(v) { defined?(ActiveStorage::Attached::Many) && v.is_a?(ActiveStorage::Attached::Many) }
|
73
|
+
handle_active_storage_many(value)
|
74
|
+
when ->(v) { defined?(ActionText::RichText) && v.is_a?(ActionText::RichText) }
|
75
|
+
handle_rich_text(value)
|
76
|
+
else
|
77
|
+
value
|
78
|
+
end
|
79
|
+
formatted_value.inspect
|
80
|
+
end
|
81
|
+
|
82
|
+
def range_to_string(range)
|
83
|
+
from = infinite_value?(range.begin) ? "" : range.begin
|
84
|
+
to = infinite_value?(range.end) ? "" : range.end
|
85
|
+
exclude_end = range.exclude_end? ? ")" : "]"
|
86
|
+
"[#{from},#{to}#{exclude_end}"
|
87
|
+
end
|
88
|
+
|
89
|
+
def rgeo_instance?(value)
|
90
|
+
value.class.ancestors.any? { |ancestor| ancestor.to_s == "RGeo::Feature::Instance" }
|
91
|
+
end
|
92
|
+
|
93
|
+
def infinite_value?(value)
|
94
|
+
value.respond_to?(:infinite?) && value.infinite?
|
95
|
+
end
|
96
|
+
|
97
|
+
def prepare_io(options)
|
98
|
+
if options[:file].present?
|
99
|
+
mode = options[:append] ? "a+" : "w+"
|
100
|
+
File.open(options[:file], mode)
|
101
|
+
else
|
102
|
+
StringIO.new
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def write_records(records, io, options) # rubocop:disable Metrics/MethodLength
|
107
|
+
options[:exclude] ||= %i[id created_at updated_at]
|
108
|
+
|
109
|
+
method_call = build_method_call(records, options)
|
110
|
+
io.write(method_call)
|
111
|
+
io.write("[\n ")
|
112
|
+
|
113
|
+
enumeration_method = select_enumeration_method(records)
|
114
|
+
send(enumeration_method, records, options) do |record_strings, last_batch|
|
115
|
+
io.write(record_strings.join(",\n "))
|
116
|
+
io.write(",\n ") unless last_batch
|
117
|
+
end
|
118
|
+
|
119
|
+
io.write("\n]#{format_import_options(options)})\n")
|
120
|
+
|
121
|
+
return if options[:file].present?
|
122
|
+
|
123
|
+
io.rewind
|
124
|
+
io.read
|
125
|
+
end
|
126
|
+
|
127
|
+
def build_method_call(records, options)
|
128
|
+
method = options[:import] ? "import" : "create!"
|
129
|
+
model_name = determine_model(records)
|
130
|
+
if options[:import]
|
131
|
+
attributes_list = attribute_names(records, options).map { _1.to_sym.inspect }.join(", ")
|
132
|
+
"#{model_name}.#{method}([#{attributes_list}], "
|
133
|
+
else
|
134
|
+
"#{model_name}.#{method}("
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
def format_import_options(options)
|
139
|
+
return "" unless options[:import].is_a?(Hash)
|
140
|
+
|
141
|
+
options_string = options[:import].map { |key, value| "#{key}: #{value}" }.join(", ")
|
142
|
+
", #{options_string}"
|
143
|
+
end
|
144
|
+
|
145
|
+
def attribute_names(records, options)
|
146
|
+
attributes = records.respond_to?(:attribute_names) ? records.attribute_names : records.first.attribute_names
|
147
|
+
attributes.reject { |name| options[:exclude].include?(name.to_sym) }
|
148
|
+
end
|
149
|
+
|
150
|
+
def determine_model(records)
|
151
|
+
if records.is_a?(Class)
|
152
|
+
records
|
153
|
+
elsif records.respond_to?(:model)
|
154
|
+
records.model
|
155
|
+
else
|
156
|
+
records.first.class
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
def select_enumeration_method(records)
|
161
|
+
if records.is_a?(ActiveRecord::Relation) || records.is_a?(Class)
|
162
|
+
:active_record_enumeration
|
163
|
+
else
|
164
|
+
:enumerable_enumeration
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
def handle_active_storage_one(attachment)
|
169
|
+
return nil unless attachment.attached?
|
170
|
+
|
171
|
+
copy_attachment_to_seeds_dir(attachment)
|
172
|
+
|
173
|
+
{
|
174
|
+
io: "File.open(Rails.root.join('db/seeds/files', '#{attachment.filename}'))",
|
175
|
+
filename: attachment.filename.to_s,
|
176
|
+
content_type: attachment.blob.content_type,
|
177
|
+
}
|
178
|
+
end
|
179
|
+
|
180
|
+
def handle_active_storage_many(attachments)
|
181
|
+
return [] unless attachments.attached?
|
182
|
+
|
183
|
+
attachments.map do |attachment|
|
184
|
+
copy_attachment_to_seeds_dir(attachment)
|
185
|
+
|
186
|
+
{
|
187
|
+
io: "File.open(Rails.root.join('db/seeds/files', '#{attachment.filename}'))",
|
188
|
+
filename: attachment.filename.to_s,
|
189
|
+
content_type: attachment.blob.content_type,
|
190
|
+
}
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
def copy_attachment_to_seeds_dir(attachment) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
195
|
+
return unless attachment&.blob
|
196
|
+
|
197
|
+
seeds_dir = Rails.root.join("db/seeds/files")
|
198
|
+
FileUtils.mkdir_p(seeds_dir)
|
199
|
+
|
200
|
+
target_path = seeds_dir.join(attachment.filename.to_s)
|
201
|
+
|
202
|
+
# Skip if file already exists
|
203
|
+
return if File.exist?(target_path)
|
204
|
+
|
205
|
+
begin
|
206
|
+
# Try to get direct file path first
|
207
|
+
source_path = attachment.blob.service.path_for(attachment.blob.key)
|
208
|
+
if source_path && File.exist?(source_path)
|
209
|
+
FileUtils.cp(source_path, target_path)
|
210
|
+
else
|
211
|
+
# Fallback to downloading for cloud storage
|
212
|
+
File.open(target_path, "wb") do |file|
|
213
|
+
attachment.download { |chunk| file.write(chunk) }
|
214
|
+
end
|
215
|
+
end
|
216
|
+
rescue StandardError => e
|
217
|
+
Rails.logger.error "Failed to copy attachment #{attachment.filename}: #{e.message}"
|
218
|
+
raise e
|
219
|
+
end
|
220
|
+
end
|
221
|
+
|
222
|
+
def handle_rich_text(rich_text)
|
223
|
+
rich_text.body&.to_html
|
224
|
+
end
|
225
|
+
end
|
226
|
+
end
|
@@ -0,0 +1,152 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class SeedDumpling
|
4
|
+
# Provides functionality for dumping database records to seed files using
|
5
|
+
# environment variables to configure the dump process. Supports model filtering,
|
6
|
+
# batch processing, and various output options.
|
7
|
+
module Environment
|
8
|
+
extend ActiveSupport::Concern
|
9
|
+
|
10
|
+
class_methods do
|
11
|
+
def dump_using_environment(...)
|
12
|
+
new.dump_using_environment(...)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def dump_using_environment(env = {}) # rubocop:disable Metrics/MethodLength
|
17
|
+
Rails.application.eager_load!
|
18
|
+
|
19
|
+
models = retrieve_models(env) - retrieve_models_exclude(env)
|
20
|
+
|
21
|
+
limit = retrieve_limit_value(env)
|
22
|
+
append = retrieve_append_value(env)
|
23
|
+
models.each do |model|
|
24
|
+
model = model.limit(limit) if limit.present?
|
25
|
+
|
26
|
+
SeedDumpling.dump(model,
|
27
|
+
append: append,
|
28
|
+
batch_size: retrieve_batch_size_value(env),
|
29
|
+
exclude: retrieve_exclude_value(env),
|
30
|
+
file: retrieve_file_value(env),
|
31
|
+
import: retrieve_import_value(env),
|
32
|
+
)
|
33
|
+
|
34
|
+
append = true # Always append for every model after the first
|
35
|
+
# (append for the first model is determined by
|
36
|
+
# the APPEND environment variable).
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
# Internal: Array of Strings corresponding to Active Record model class names
|
43
|
+
# that should be excluded from the dump.
|
44
|
+
ACTIVE_RECORD_INTERNAL_MODELS = ["ActiveRecord::SchemaMigration",
|
45
|
+
"ActiveRecord::InternalMetadata"].freeze
|
46
|
+
|
47
|
+
# Internal: Retrieves an Array of Active Record model class constants to be
|
48
|
+
# dumped.
|
49
|
+
#
|
50
|
+
# If a "MODEL" or "MODELS" environment variable is specified, there will be
|
51
|
+
# an attempt to parse the environment variable String by splitting it on
|
52
|
+
# commmas and then converting it to constant.
|
53
|
+
#
|
54
|
+
# Model classes that do not have corresponding database tables or database
|
55
|
+
# records will be filtered out, as will model classes internal to Active
|
56
|
+
# Record.
|
57
|
+
#
|
58
|
+
# env - Hash of environment variables from which to parse Active Record
|
59
|
+
# model classes. The Hash is not optional but the "MODEL" and "MODELS"
|
60
|
+
# keys are optional.
|
61
|
+
#
|
62
|
+
# Returns the Array of Active Record model classes to be dumped.
|
63
|
+
def retrieve_models(env) # rubocop:disable Metrics/AbcSize
|
64
|
+
# Parse either the "MODEL" environment variable or the "MODELS"
|
65
|
+
# environment variable, with "MODEL" taking precedence.
|
66
|
+
models_env = env["MODEL"] || env["MODELS"]
|
67
|
+
|
68
|
+
# If there was a use models environment variable, split it and
|
69
|
+
# convert the given model string (e.g. "User") to an actual
|
70
|
+
# model constant (e.g. User).
|
71
|
+
#
|
72
|
+
# If a models environment variable was not given, use descendants of
|
73
|
+
# ActiveRecord::Base as the target set of models. This should be all
|
74
|
+
# model classes in the project.
|
75
|
+
models = if models_env
|
76
|
+
models_env.split(",")
|
77
|
+
.collect { |x| x.strip.underscore.singularize.camelize.constantize }
|
78
|
+
else
|
79
|
+
ActiveRecord::Base.descendants
|
80
|
+
end
|
81
|
+
|
82
|
+
# Filter the set of models to exclude:
|
83
|
+
# - The ActiveRecord::SchemaMigration model which is internal to Rails
|
84
|
+
# and should not be part of the dumped data.
|
85
|
+
# - Models that don't have a corresponding table in the database.
|
86
|
+
# - Models whose corresponding database tables are empty.
|
87
|
+
models.select do |model|
|
88
|
+
ACTIVE_RECORD_INTERNAL_MODELS.exclude?(model.to_s) &&
|
89
|
+
model.table_exists? &&
|
90
|
+
model.exists?
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
# Internal: Returns a Boolean indicating whether the value for the "APPEND"
|
95
|
+
# key in the given Hash is equal to the String "true" (ignoring case),
|
96
|
+
# false if no value exists.
|
97
|
+
def retrieve_append_value(env)
|
98
|
+
parse_boolean_value(env["APPEND"])
|
99
|
+
end
|
100
|
+
|
101
|
+
# Internal: Returns a Boolean indicating whether the value for the "IMPORT"
|
102
|
+
# key in the given Hash is equal to the String "true" (ignoring case),
|
103
|
+
# false if no value exists.
|
104
|
+
def retrieve_import_value(env)
|
105
|
+
parse_boolean_value(env["IMPORT"])
|
106
|
+
end
|
107
|
+
|
108
|
+
# Internal: Retrieves an Array of Class constants parsed from the value for
|
109
|
+
# the "MODELS_EXCLUDE" key in the given Hash, and an empty Array if such
|
110
|
+
# key exists.
|
111
|
+
def retrieve_models_exclude(env)
|
112
|
+
env["MODELS_EXCLUDE"].to_s
|
113
|
+
.split(",")
|
114
|
+
.collect { |x| x.strip.underscore.singularize.camelize.constantize }
|
115
|
+
end
|
116
|
+
|
117
|
+
# Internal: Retrieves an Integer from the value for the "LIMIT" key in the
|
118
|
+
# given Hash, and nil if no such key exists.
|
119
|
+
def retrieve_limit_value(env)
|
120
|
+
retrieve_integer_value("LIMIT", env)
|
121
|
+
end
|
122
|
+
|
123
|
+
# Internal: Retrieves an Array of Symbols from the value for the "EXCLUDE"
|
124
|
+
# key from the given Hash, and nil if no such key exists.
|
125
|
+
def retrieve_exclude_value(env)
|
126
|
+
env["EXCLUDE"]&.split(",")&.map { |e| e.strip.to_sym }
|
127
|
+
end
|
128
|
+
|
129
|
+
# Internal: Retrieves the value for the "FILE" key from the given Hash, and
|
130
|
+
# 'db/seeds.rb' if no such key exists.
|
131
|
+
def retrieve_file_value(env)
|
132
|
+
env["FILE"] || "db/seeds.rb"
|
133
|
+
end
|
134
|
+
|
135
|
+
# Internal: Retrieves an Integer from the value for the "BATCH_SIZE" key in
|
136
|
+
# the given Hash, and nil if no such key exists.
|
137
|
+
def retrieve_batch_size_value(env)
|
138
|
+
retrieve_integer_value("BATCH_SIZE", env)
|
139
|
+
end
|
140
|
+
|
141
|
+
# Internal: Retrieves an Integer from the value for the given key in
|
142
|
+
# the given Hash, and nil if no such key exists.
|
143
|
+
def retrieve_integer_value(key, hash)
|
144
|
+
hash[key]&.to_i
|
145
|
+
end
|
146
|
+
|
147
|
+
# Internal: Parses a Boolean from the given value.
|
148
|
+
def parse_boolean_value(value)
|
149
|
+
value.to_s.downcase == "true"
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "ipaddr"
|
4
|
+
require "seed_dumpling/dump_methods/enumeration"
|
5
|
+
require "seed_dumpling/dump_methods"
|
6
|
+
require "seed_dumpling/environment"
|
7
|
+
|
8
|
+
# Tool for extracting database records into a format suitable for
|
9
|
+
# Rails' db/seeds.rb. Supports various data types and configurable output formats.
|
10
|
+
class SeedDumpling
|
11
|
+
include Environment
|
12
|
+
include DumpMethods
|
13
|
+
|
14
|
+
require "seed_dumpling/railtie" if defined?(Rails)
|
15
|
+
end
|
metadata
ADDED
@@ -0,0 +1,87 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: seed_dumpling
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Mikael Henriksson
|
8
|
+
bindir: bin
|
9
|
+
cert_chain: []
|
10
|
+
date: 2025-01-06 00:00:00.000000000 Z
|
11
|
+
dependencies:
|
12
|
+
- !ruby/object:Gem::Dependency
|
13
|
+
name: activerecord-import
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
15
|
+
requirements:
|
16
|
+
- - ">="
|
17
|
+
- !ruby/object:Gem::Version
|
18
|
+
version: '0'
|
19
|
+
type: :runtime
|
20
|
+
prerelease: false
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
22
|
+
requirements:
|
23
|
+
- - ">="
|
24
|
+
- !ruby/object:Gem::Version
|
25
|
+
version: '0'
|
26
|
+
- !ruby/object:Gem::Dependency
|
27
|
+
name: rails
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
29
|
+
requirements:
|
30
|
+
- - ">="
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '7.0'
|
33
|
+
- - "<"
|
34
|
+
- !ruby/object:Gem::Version
|
35
|
+
version: '9.0'
|
36
|
+
type: :runtime
|
37
|
+
prerelease: false
|
38
|
+
version_requirements: !ruby/object:Gem::Requirement
|
39
|
+
requirements:
|
40
|
+
- - ">="
|
41
|
+
- !ruby/object:Gem::Version
|
42
|
+
version: '7.0'
|
43
|
+
- - "<"
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '9.0'
|
46
|
+
description: Dump (parts) of your database to db/seedspec.rb to get a headstart creating
|
47
|
+
a meaningful seedspec.rb file.
|
48
|
+
email: mikael@mhenrixon.com
|
49
|
+
executables: []
|
50
|
+
extensions: []
|
51
|
+
extra_rdoc_files: []
|
52
|
+
files:
|
53
|
+
- LICENSE.txt
|
54
|
+
- README.md
|
55
|
+
- lib/seed_dumpling.rb
|
56
|
+
- lib/seed_dumpling/dump_methods.rb
|
57
|
+
- lib/seed_dumpling/dump_methods/enumeration.rb
|
58
|
+
- lib/seed_dumpling/environment.rb
|
59
|
+
- lib/seed_dumpling/railtie.rb
|
60
|
+
- lib/seed_dumpling/version.rb
|
61
|
+
- lib/tasks/seed_dumpling.rake
|
62
|
+
homepage: https://github.com/mhenrixon/dumpling
|
63
|
+
licenses:
|
64
|
+
- MIT
|
65
|
+
metadata:
|
66
|
+
rubygems_mfa_required: 'true'
|
67
|
+
homepage_uri: https://github.com/mhenrixon/dumpling
|
68
|
+
source_code_uri: https://github.com/mhenrixon/dumpling
|
69
|
+
changelog_uri: https://github.com/mhenrixon/dumpling/blob/up/CHANGELOG.md
|
70
|
+
rdoc_options: []
|
71
|
+
require_paths:
|
72
|
+
- lib
|
73
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
74
|
+
requirements:
|
75
|
+
- - ">="
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: 3.2.0
|
78
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
requirements: []
|
84
|
+
rubygems_version: 3.6.2
|
85
|
+
specification_version: 4
|
86
|
+
summary: Seed Dumper for Rails
|
87
|
+
test_files: []
|