brick 1.0.228 → 1.0.230

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.
@@ -0,0 +1,329 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fancy_gets'
4
+
5
+ module Brick
6
+ class SeedsBuilder
7
+ class << self
8
+ include FancyGets
9
+
10
+ SeedModel = Struct.new(:table_name, :klass, :is_brick, :airtable_table)
11
+ SeedModel.define_method(:to_s) do
12
+ "#{klass.name}#{' (brick-generated)' if is_brick}"
13
+ end
14
+
15
+ def generate_seeds(relations = nil)
16
+ if File.exist?(seed_file_path = "#{::Rails.root}/db/seeds.rb")
17
+ puts "WARNING: seeds file #{seed_file_path} appears to already be present.\nOverwrite?"
18
+ return unless gets_list(list: ['No', 'Yes']) == 'Yes'
19
+
20
+ puts "\n"
21
+ end
22
+
23
+ if relations
24
+ is_airtable = true # So far the only thing that feeds us relations is Airtable
25
+ require 'generators/brick/airtable_api_caller'
26
+ # include ::Brick::MigrationsBuilder
27
+ chosen = relations.map { |k, v| SeedModel.new(k, nil, false, v[:airtable_table]) }
28
+ else
29
+ ::Brick.mode = :on
30
+ ActiveRecord::Base.establish_connection
31
+ relations = ::Brick.relations
32
+
33
+ # Load all models
34
+ ::Brick.eager_load_classes
35
+
36
+ # Generate a list of viable models that can be chosen
37
+ # First start with any existing models that have been defined ...
38
+ existing_models = ActiveRecord::Base.descendants.each_with_object({}) do |m, s|
39
+ s[m.table_name] = SeedModel.new(m.table_name, m, false) if !m.abstract_class? && !m.is_view? && m.table_exists?
40
+ end
41
+
42
+ models = (existing_models.values +
43
+ # ... then add models which can be auto-built by Brick
44
+ relations.reject do |k, v|
45
+ k.is_a?(Symbol) || (v.key?(:isView) && v[:isView] == true) || existing_models.key?(k)
46
+ end.map { |k, v| SeedModel.new(k, v[:class_name].constantize, true) }
47
+ ).sort { |a, b| a.to_s <=> b.to_s }
48
+ if models.empty?
49
+ puts "No viable models found for database #{ActiveRecord::Base.connection.current_database}."
50
+ return
51
+ end
52
+
53
+ chosen = gets_list(list: models, chosen: models.dup)
54
+ schemas = chosen.each_with_object({}) do |v, s|
55
+ if (v_parts = v.table_name.split('.')).length > 1
56
+ s[v_parts.first] = nil unless [::Brick.default_schema, 'public'].include?(v_parts.first)
57
+ end
58
+ end
59
+ end
60
+
61
+ seeds = +'# Seeds file for '
62
+ if (arbc = ActiveRecord::Base.connection).respond_to?(:current_database) # SQLite3 can't do this!
63
+ seeds << "#{arbc.current_database}:\n"
64
+ elsif (filename = arbc.instance_variable_get(:@connection_parameters)&.fetch(:database, nil))
65
+ seeds << "#{filename}:\n"
66
+ end
67
+ done = []
68
+ fks = {}
69
+ stuck = {}
70
+ indexes = {} # Track index names to make sure things are unique
71
+ ar_base = Object.const_defined?(:ApplicationRecord) ? ApplicationRecord : Class.new(ActiveRecord::Base)
72
+ atrt_idx = 0 # ActionText::RichText unique index number
73
+ airtable_assoc_recids = Hash.new { |h, k| h[k] = [] }
74
+ @has_atrts = nil # Any ActionText::RichText present?
75
+ # Start by making entries for fringe models (those with no foreign keys).
76
+ # Continue layer by layer, creating entries for models that reference ones already done, until
77
+ # no more entries can be created. (At that point hopefully all models are accounted for.)
78
+ while (fringe = chosen.reject do |seed_model|
79
+ tbl = seed_model.table_name
80
+ snag_fks = []
81
+ snags = relations.fetch(tbl, nil)&.fetch(:fks, nil)&.select do |_k, v|
82
+ # Skip any foreign keys which should be deferred ...
83
+ !Brick.drfgs[tbl]&.any? do |drfg|
84
+ drfg[0] == v.fetch(:fk, nil) && drfg[1] == v.fetch(:inverse_table, nil)
85
+ end &&
86
+ v[:is_bt] && !v[:polymorphic] && # ... and polymorphics ...
87
+ tbl != v[:inverse_table] && # ... and self-referencing associations (stuff like "parent_id")
88
+ !done.any? { |done_seed_model| done_seed_model.table_name == v[:inverse_table] } &&
89
+ ::Brick.config.ignore_migration_fks.exclude?(snag_fk = "#{tbl}.#{v[:fk]}") &&
90
+ snag_fks << snag_fk
91
+ end
92
+ if snags&.present?
93
+ # puts snag_fks.inspect
94
+ stuck[tbl] = snags
95
+ end
96
+ end
97
+ ).present?
98
+ seeds << "\n"
99
+ unless is_airtable
100
+ # Search through the fringe to see if we should bump special dependent classes forward to the next fringe.
101
+ # (Currently only ActiveStorage::Attachment if there's also an ActiveStorage::VariantRecord in the same
102
+ # fringe, and always have ActionText::EncryptedRichText at the very end.)
103
+ fringe_classes = fringe.map { |f| f.klass.name }
104
+ unless (asa_idx = fringe_classes.index('ActiveStorage::Attachment')).nil?
105
+ fringe.slice!(asa_idx) if fringe_classes.include?('ActiveStorage::VariantRecord')
106
+ end
107
+ unless (atert_idx = fringe_classes.index('ActionText::EncryptedRichText')).nil?
108
+ fringe.slice!(atert_idx) if fringe_classes.length > 1
109
+ end
110
+ end
111
+ fringe.each do |seed_model|
112
+ tbl = seed_model.table_name
113
+ next unless ::Brick.config.exclude_tables.exclude?(tbl) &&
114
+ (relation = relations.fetch(tbl, nil))&.fetch(:cols, nil)&.present? &&
115
+ (is_airtable || (klass = seed_model.klass).table_exists?)
116
+
117
+ pkey_cols = (rpk = relation[:pkey].values.flatten) & (arpk = [ar_base.primary_key].flatten.sort)
118
+ # In case things aren't as standard
119
+ if pkey_cols.empty?
120
+ pkey_cols = if rpk.empty? # && relation[:cols][arpk.first]&.first == key_type
121
+ arpk
122
+ elsif rpk.first
123
+ rpk
124
+ end
125
+ end
126
+ schema = if (tbl_parts = tbl.split('.')).length > 1
127
+ if tbl_parts.first == (::Brick.default_schema || 'public')
128
+ tbl_parts.shift
129
+ nil
130
+ else
131
+ tbl_parts.first
132
+ end
133
+ end
134
+
135
+ # %%% For the moment we're skipping polymorphics
136
+ fkeys = if is_airtable
137
+ tbl = tbl.singularize
138
+ relation[:fks]&.values&.select { |assoc| assoc[:is_bt] && !assoc[:polymorphic] }
139
+ else
140
+ klass.reflect_on_all_associations.select { |a| a.belongs_to? && !a.polymorphic? }.map do |fk|
141
+ { fk: fk.foreign_key, assoc_name: fk.name.to_s, inverse_table: fk.table_name }
142
+ end
143
+ end
144
+ # Refer to this table name as a symbol or dotted string as appropriate
145
+ # tbl_code = tbl_parts.length == 1 ? ":#{tbl_parts.first}" : "'#{tbl}'"
146
+
147
+ has_rows = false
148
+ is_empty = true
149
+ klass_name = is_airtable ? ::Brick::AirtableApiCaller.sane_table_name(relation[:airtable_table]&.name)&.singularize&.camelize : klass.name
150
+ # Pull the records
151
+ collection = if is_airtable
152
+ if (airtable_table = relation[:airtable_table])
153
+ ::Brick::AirtableApiCaller.https_get("https://api.airtable.com/v0/#{airtable_table.base_id}/#{airtable_table.id}").fetch('records', nil)
154
+ end
155
+ else
156
+ klass.order(*pkey_cols)
157
+ end
158
+ collection&.each do |obj|
159
+ if is_airtable
160
+ fields = obj['fields'].each_with_object({}) do |field, s|
161
+ if relation[:cols].keys.include?(col_name = ::Brick::AirtableApiCaller.sane_name(field.first))
162
+ s[col_name] = obj['fields'][field.first]
163
+ else # Consider N:M fks
164
+ nm_fk = relation[:fks].find do |_k, fk1|
165
+ relations[fk1[:assoc_tbl]]&.fetch(:fks, nil)&.find { |_k, fk2| fk2[:assoc_name] == col_name }
166
+ end&.last
167
+ if (t_table = nm_fk&.fetch(:inverse_table, nil))
168
+ field.last.each do |nm_rec|
169
+ nm_fk_col = nm_fk[:assoc_tbl]
170
+ airtable_assoc_recids[t_table] << "#{nm_fk[:fk]}: #{nm_fk[:fk].singularize}_#{obj['id'][3..-1]}, " \
171
+ "#{nm_fk_col}: #{nm_fk_col.singularize}_#{nm_rec[3..-1]}"
172
+ end
173
+ end
174
+ end
175
+ end
176
+ objects = relation[:airtable_table].objects
177
+ obj = objects[airtable_id = obj['id']] = AirtableObject.new(seed_model, obj['fields'], obj['createdTime'])
178
+ end
179
+ unless has_rows
180
+ has_rows = true
181
+ seeds << " puts 'Seeding: #{klass_name}'\n"
182
+ end
183
+ is_empty = false
184
+ # For Airtable, take off the "rec___" prefix
185
+ pk_val = is_airtable ? airtable_id[3..-1] : brick_escape(obj.attributes_before_type_cast[pkey_cols.first])
186
+ var_name = "#{tbl.gsub('.', '__')}_#{pk_val}"
187
+ fk_vals = []
188
+ data = []
189
+ updates = []
190
+ relation[:cols].each do |col, _col_type|
191
+ # Skip primary key columns, unless they are part of a foreign key.
192
+ # (But always add all columns if it's Airtable!)
193
+ next if !(fk = fkeys.find { |assoc| col == assoc[:fk] }) &&
194
+ pkey_cols.include?(col) &&
195
+ !is_airtable
196
+
197
+ # Used to be: obj.send(col)
198
+ # (and with that it was possible to raise ActiveRecord::Encryption::Errors::Configuration...)
199
+ # %%% should test further and see if that is possible with this code!)
200
+ if (val = obj.attributes_before_type_cast[col]) && (val.is_a?(Time) || val.is_a?(Date))
201
+ val = val.to_s
202
+ end
203
+ if fk
204
+ inv_tbl = fk[:inverse_table].gsub('.', '__')
205
+ fk_val = if is_airtable
206
+ # Used to be: fk[:airtable_col]
207
+ # Take off the "rec___" prefix
208
+ obj.attributes_before_type_cast[fk[:assoc_name]]&.first&.[](3..-1)
209
+ else
210
+ brick_escape(val)
211
+ end
212
+ fk_vals << "#{fk[:assoc_name]}: #{inv_tbl}_#{fk_val}" if fk_val
213
+ else
214
+ val = case val.class.name
215
+ when 'ActiveStorage::Filename'
216
+ val.to_s.inspect
217
+ when 'ActionText::RichText'
218
+ ensure_has_atrts(updates)
219
+ atrt_var = "atrt#{atrt_idx += 1}"
220
+ atrt_create = "(#{atrt_var} = #{val.class.name}.create(name: #{val.name.inspect}, body: #{val.to_trix_html.inspect
221
+ }, record_type: #{val.record_type.inspect}, record_id: #{var_name}.#{pkey_cols.first
222
+ }, created_at: DateTime.parse('#{val.created_at.inspect}'), updated_at: DateTime.parse('#{val.updated_at.inspect}')))"
223
+ updates << "#{var_name}.update(#{col}: #{atrt_create})\n"
224
+ # obj.send(col)&.embeds_blobs&.each do |blob|
225
+ updates << "atrt_ids[[#{val.id}, '#{val.class.name}']] = #{atrt_var}.id\n"
226
+ # end
227
+ next
228
+ else
229
+ val.inspect
230
+ end
231
+ data << "#{col}: #{val}" unless val == 'nil'
232
+ end
233
+ end
234
+ case klass_name
235
+ when 'ActiveStorage::VariantRecord'
236
+ ensure_has_atrts(updates)
237
+ updates << "atrt_ids[[#{obj.id}, '#{klass_name}']] = #{var_name}.id\n"
238
+ end
239
+ # Make sure that ActiveStorage::Attachment and ActionText::EncryptedRichText get
240
+ # wired up to the proper record_id
241
+ if klass_name == 'ActiveStorage::Attachment' || klass_name == 'ActionText::EncryptedRichText'
242
+ record_class = data.find { |d| d.start_with?('record_type: ') }[14..-2]
243
+ record_id = data.find { |d| d.start_with?('record_id: ') }[11..-1]
244
+ data.reject! { |d| d.start_with?('record_id: ') || d.start_with?('created_at: ') || d.start_with?('updated_at: ') }
245
+ data << "record_id: atrt_ids[[#{record_id}, '#{record_class}']]"
246
+ seeds << "#{var_name} = #{klass_name}.find_or_create_by(#{(fk_vals + data).join(', ')}) do |asa|
247
+ asa.created_at = DateTime.parse('#{obj.created_at.inspect}')#{"
248
+ asa.updated_at = DateTime.parse('#{obj.updated_at.inspect}')" if obj.respond_to?(:updated_at)}
249
+ end\n"
250
+ else
251
+ seeds << "#{var_name} = #{klass_name}.create(#{(fk_vals + data).join(', ')})\n"
252
+ unless is_airtable
253
+ klass.attachment_reflections.each do |k, v|
254
+ if (attached = obj.send(k))
255
+ ensure_has_atrts(updates)
256
+ updates << "atrt_ids[[#{obj.id}, '#{klass_name}']] = #{var_name}.id\n"
257
+ end
258
+ end if klass.respond_to?(:attachment_reflections)
259
+ end
260
+ end
261
+ updates.each { |update| seeds << update } # Anything that needs patching up after-the-fact
262
+ end
263
+ seeds << " # (Skipping #{klass_name} as it has no rows)\n" unless has_rows
264
+ end
265
+ done.concat(fringe)
266
+ chosen -= done
267
+ end
268
+ airtable_assoc_recids.each do |k, v| # N:M links
269
+ v.each do |link|
270
+ seeds << "#{k.singularize.camelize}.create(#{link})\n"
271
+ end
272
+ end
273
+
274
+ File.open(seed_file_path, "w") { |f| f.write seeds }
275
+ stuck_counts = Hash.new { |h, k| h[k] = 0 }
276
+ chosen.each do |leftover|
277
+ puts "Can't do #{leftover.klass_name} because:\n #{stuck[leftover.table_name].map do |snag|
278
+ stuck_counts[snag.last[:inverse_table]] += 1
279
+ snag.last[:assoc_name]
280
+ end.join(', ')}"
281
+ end
282
+ puts "\n*** Created seeds for #{done.length} models in db/seeds.rb ***"
283
+ if (stuck_sorted = stuck_counts.to_a.sort { |a, b| b.last <=> a.last }).length.positive?
284
+ puts "-----------------------------------------"
285
+ puts "Unable to create seeds for #{stuck_sorted.length} tables#{
286
+ ". Here's the top 5 blockers" if stuck_sorted.length > 5
287
+ }:"
288
+ pp stuck_sorted[0..4]
289
+ end
290
+ end
291
+
292
+ private
293
+
294
+ def brick_escape(val)
295
+ val = val.to_s if val.is_a?(Date) || val.is_a?(Time) # Accommodate when for whatever reason a primary key is a date or time
296
+ case val
297
+ when String
298
+ ret = +''
299
+ val.each_char do |ch|
300
+ if ch < '0' || (ch > '9' && ch < 'A') || ch > 'Z'
301
+ ret << (ch == '_' ? ch : "x#{'K'.unpack('H*')[0]}")
302
+ else
303
+ ret << ch
304
+ end
305
+ end
306
+ ret
307
+ else
308
+ val
309
+ end
310
+ end
311
+
312
+ def ensure_has_atrts(array)
313
+ unless @has_atrts
314
+ array << "atrt_ids = {}\n"
315
+ @has_atrts = true
316
+ end
317
+ end
318
+
319
+ class AirtableObject
320
+ attr_accessor :table, :attributes_before_type_cast, :created_at
321
+ def initialize(table, attributes, created_at)
322
+ self.table = table
323
+ self.attributes_before_type_cast = attributes.each_with_object({}) { |a, s| s[::Brick::AirtableApiCaller.sane_name(a.first)] = a.last }
324
+ self.created_at = created_at
325
+ end
326
+ end
327
+ end
328
+ end
329
+ end
@@ -2,256 +2,16 @@
2
2
 
3
3
  require 'brick'
4
4
  require 'rails/generators'
5
- require 'fancy_gets'
5
+ require 'generators/brick/seeds_builder'
6
6
 
7
7
  module Brick
8
8
  class SeedsGenerator < ::Rails::Generators::Base
9
- include FancyGets
10
-
11
9
  desc 'Auto-generates a seeds file from existing data.'
12
10
 
13
- SeedModel = Struct.new(:table_name, :klass, :is_brick)
14
- SeedModel.define_method(:to_s) do
15
- "#{klass.name}#{' (brick-generated)' if is_brick}"
16
- end
17
-
18
11
  def brick_seeds
19
12
  # %%% If Apartment is active and there's no schema_to_analyse, ask which schema they want
20
13
 
21
- ::Brick.mode = :on
22
- ActiveRecord::Base.establish_connection
23
-
24
- # Load all models
25
- ::Brick.eager_load_classes
26
-
27
- # Generate a list of viable models that can be chosen
28
- # First start with any existing models that have been defined ...
29
- existing_models = ActiveRecord::Base.descendants.each_with_object({}) do |m, s|
30
- s[m.table_name] = SeedModel.new(m.table_name, m, false) if !m.abstract_class? && !m.is_view? && m.table_exists?
31
- end
32
- models = (existing_models.values +
33
- # ... then add models which can be auto-built by Brick
34
- ::Brick.relations.reject do |k, v|
35
- k.is_a?(Symbol) || (v.key?(:isView) && v[:isView] == true) || existing_models.key?(k)
36
- end.map { |k, v| SeedModel.new(k, v[:class_name].constantize, true) }
37
- ).sort { |a, b| a.to_s <=> b.to_s }
38
- if models.empty?
39
- puts "No viable models found for database #{ActiveRecord::Base.connection.current_database}."
40
- return
41
- end
42
-
43
- if File.exist?(seed_file_path = "#{::Rails.root}/db/seeds.rb")
44
- puts "WARNING: seeds file #{seed_file_path} appears to already be present.\nOverwrite?"
45
- return unless gets_list(list: ['No', 'Yes']) == 'Yes'
46
-
47
- puts "\n"
48
- end
49
-
50
- chosen = gets_list(list: models, chosen: models.dup)
51
- schemas = chosen.each_with_object({}) do |v, s|
52
- if (v_parts = v.table_name.split('.')).length > 1
53
- s[v_parts.first] = nil unless [::Brick.default_schema, 'public'].include?(v_parts.first)
54
- end
55
- end
56
- seeds = +'# Seeds file for '
57
- if (arbc = ActiveRecord::Base.connection).respond_to?(:current_database) # SQLite3 can't do this!
58
- seeds << "#{arbc.current_database}:\n"
59
- elsif (filename = arbc.instance_variable_get(:@connection_parameters)&.fetch(:database, nil))
60
- seeds << "#{filename}:\n"
61
- end
62
- done = []
63
- fks = {}
64
- stuck = {}
65
- indexes = {} # Track index names to make sure things are unique
66
- ar_base = Object.const_defined?(:ApplicationRecord) ? ApplicationRecord : Class.new(ActiveRecord::Base)
67
- atrt_idx = 0 # ActionText::RichText unique index number
68
- @has_atrts = nil # Any ActionText::RichText present?
69
- # Start by making entries for fringe models (those with no foreign keys).
70
- # Continue layer by layer, creating entries for models that reference ones already done, until
71
- # no more entries can be created. (At that point hopefully all models are accounted for.)
72
- while (fringe = chosen.reject do |seed_model|
73
- tbl = seed_model.table_name
74
- snag_fks = []
75
- snags = ::Brick.relations.fetch(tbl, nil)&.fetch(:fks, nil)&.select do |_k, v|
76
- # Skip any foreign keys which should be deferred ...
77
- !Brick.drfgs[tbl]&.any? do |drfg|
78
- drfg[0] == v.fetch(:fk, nil) && drfg[1] == v.fetch(:inverse_table, nil)
79
- end &&
80
- v[:is_bt] && !v[:polymorphic] && # ... and polymorphics ...
81
- tbl != v[:inverse_table] && # ... and self-referencing associations (stuff like "parent_id")
82
- !done.any? { |done_seed_model| done_seed_model.table_name == v[:inverse_table] } &&
83
- ::Brick.config.ignore_migration_fks.exclude?(snag_fk = "#{tbl}.#{v[:fk]}") &&
84
- snag_fks << snag_fk
85
- end
86
- if snags&.present?
87
- # puts snag_fks.inspect
88
- stuck[tbl] = snags
89
- end
90
- end
91
- ).present?
92
- seeds << "\n"
93
- # Search through the fringe to see if we should bump special dependent classes forward to the next fringe.
94
- # (Currently only ActiveStorage::Attachment if there's also an ActiveStorage::VariantRecord in the same
95
- # fringe, and always have ActionText::EncryptedRichText at the very end.)
96
- fringe_classes = fringe.map { |f| f.klass.name }
97
- unless (asa_idx = fringe_classes.index('ActiveStorage::Attachment')).nil?
98
- fringe.slice!(asa_idx) if fringe_classes.include?('ActiveStorage::VariantRecord')
99
- end
100
- unless (atert_idx = fringe_classes.index('ActionText::EncryptedRichText')).nil?
101
- fringe.slice!(atert_idx) if fringe_classes.length > 1
102
- end
103
- fringe.each do |seed_model|
104
- tbl = seed_model.table_name
105
- next unless ::Brick.config.exclude_tables.exclude?(tbl) &&
106
- (relation = ::Brick.relations.fetch(tbl, nil))&.fetch(:cols, nil)&.present? &&
107
- (klass = seed_model.klass).table_exists?
108
-
109
- pkey_cols = (rpk = relation[:pkey].values.flatten) & (arpk = [ar_base.primary_key].flatten.sort)
110
- # In case things aren't as standard
111
- if pkey_cols.empty?
112
- pkey_cols = if rpk.empty? # && relation[:cols][arpk.first]&.first == key_type
113
- arpk
114
- elsif rpk.first
115
- rpk
116
- end
117
- end
118
- schema = if (tbl_parts = tbl.split('.')).length > 1
119
- if tbl_parts.first == (::Brick.default_schema || 'public')
120
- tbl_parts.shift
121
- nil
122
- else
123
- tbl_parts.first
124
- end
125
- end
126
-
127
- # %%% For the moment we're skipping polymorphics
128
- fkeys = relation[:fks].values.select { |assoc| assoc[:is_bt] && !assoc[:polymorphic] }
129
- # Refer to this table name as a symbol or dotted string as appropriate
130
- # tbl_code = tbl_parts.length == 1 ? ":#{tbl_parts.first}" : "'#{tbl}'"
131
-
132
- has_rows = false
133
- is_empty = true
134
- klass.order(*pkey_cols).each do |obj|
135
- unless has_rows
136
- has_rows = true
137
- seeds << " puts 'Seeding: #{klass.name}'\n"
138
- end
139
- is_empty = false
140
- pk_val = obj.send(pkey_cols.first)
141
- var_name = "#{tbl.gsub('.', '__')}_#{brick_escape(pk_val)}"
142
- fk_vals = []
143
- data = []
144
- updates = []
145
- relation[:cols].each do |col, _col_type|
146
- next if !(fk = fkeys.find { |assoc| col == assoc[:fk] }) &&
147
- pkey_cols.include?(col)
148
-
149
- begin
150
- # Used to be: obj.send(col)
151
- if (val = obj.attributes_before_type_cast[col]) && (val.is_a?(Time) || val.is_a?(Date))
152
- val = val.to_s
153
- end
154
- rescue StandardError => e # ActiveRecord::Encryption::Errors::Configuration
155
- end
156
- if fk
157
- inv_tbl = fk[:inverse_table].gsub('.', '__')
158
- fk_vals << "#{fk[:assoc_name]}: #{inv_tbl}_#{brick_escape(val)}" if val
159
- else
160
- val = case val.class.name
161
- when 'ActiveStorage::Filename'
162
- val.to_s.inspect
163
- when 'ActionText::RichText'
164
- ensure_has_atrts(updates)
165
- atrt_var = "atrt#{atrt_idx += 1}"
166
- atrt_create = "(#{atrt_var} = #{val.class.name}.create(name: #{val.name.inspect}, body: #{val.to_trix_html.inspect
167
- }, record_type: #{val.record_type.inspect}, record_id: #{var_name}.#{pkey_cols.first
168
- }, created_at: DateTime.parse('#{val.created_at.inspect}'), updated_at: DateTime.parse('#{val.updated_at.inspect}')))"
169
- updates << "#{var_name}.update(#{col}: #{atrt_create})\n"
170
- # obj.send(col)&.embeds_blobs&.each do |blob|
171
- updates << "atrt_ids[[#{val.id}, '#{val.class.name}']] = #{atrt_var}.id\n"
172
- # end
173
- next
174
- else
175
- val.inspect
176
- end
177
- data << "#{col}: #{val}" unless val == 'nil'
178
- end
179
- end
180
- case klass.name
181
- when 'ActiveStorage::VariantRecord'
182
- ensure_has_atrts(updates)
183
- updates << "atrt_ids[[#{obj.id}, '#{klass.name}']] = #{var_name}.id\n"
184
- end
185
- # Make sure that ActiveStorage::Attachment and ActionText::EncryptedRichText get
186
- # wired up to the proper record_id
187
- if klass.name == 'ActiveStorage::Attachment' || klass.name == 'ActionText::EncryptedRichText'
188
- record_class = data.find { |d| d.start_with?('record_type: ') }[14..-2]
189
- record_id = data.find { |d| d.start_with?('record_id: ') }[11..-1]
190
- data.reject! { |d| d.start_with?('record_id: ') || d.start_with?('created_at: ') || d.start_with?('updated_at: ') }
191
- data << "record_id: atrt_ids[[#{record_id}, '#{record_class}']]"
192
- seeds << "#{var_name} = #{klass.name}.find_or_create_by(#{(fk_vals + data).join(', ')}) do |asa|
193
- asa.created_at = DateTime.parse('#{obj.created_at.inspect}')#{"
194
- asa.updated_at = DateTime.parse('#{obj.updated_at.inspect}')" if obj.respond_to?(:updated_at)}
195
- end\n"
196
- else
197
- seeds << "#{var_name} = #{seed_model.klass.name}.create(#{(fk_vals + data).join(', ')})\n"
198
- klass.attachment_reflections.each do |k, v|
199
- if (attached = obj.send(k))
200
- ensure_has_atrts(updates)
201
- updates << "atrt_ids[[#{obj.id}, '#{klass.name}']] = #{var_name}.id\n"
202
- end
203
- end if klass.respond_to?(:attachment_reflections)
204
- end
205
- updates.each { |update| seeds << update } # Anything that needs patching up after-the-fact
206
- end
207
- seeds << " # (Skipping #{seed_model.klass.name} as it has no rows)\n" unless has_rows
208
- File.open(seed_file_path, "w") { |f| f.write seeds }
209
- end
210
- done.concat(fringe)
211
- chosen -= done
212
- end
213
- stuck_counts = Hash.new { |h, k| h[k] = 0 }
214
- chosen.each do |leftover|
215
- puts "Can't do #{leftover.klass.name} because:\n #{stuck[leftover.table_name].map do |snag|
216
- stuck_counts[snag.last[:inverse_table]] += 1
217
- snag.last[:assoc_name]
218
- end.join(', ')}"
219
- end
220
- puts "\n*** Created seeds for #{done.length} models in db/seeds.rb ***"
221
- if (stuck_sorted = stuck_counts.to_a.sort { |a, b| b.last <=> a.last }).length.positive?
222
- puts "-----------------------------------------"
223
- puts "Unable to create seeds for #{stuck_sorted.length} tables#{
224
- ". Here's the top 5 blockers" if stuck_sorted.length > 5
225
- }:"
226
- pp stuck_sorted[0..4]
227
- end
228
- end
229
-
230
- private
231
-
232
- def brick_escape(val)
233
- val = val.to_s if val.is_a?(Date) || val.is_a?(Time) # Accommodate when for whatever reason a primary key is a date or time
234
- case val
235
- when String
236
- ret = +''
237
- val.each_char do |ch|
238
- if ch < '0' || (ch > '9' && ch < 'A') || ch > 'Z'
239
- ret << (ch == '_' ? ch : "x#{'K'.unpack('H*')[0]}")
240
- else
241
- ret << ch
242
- end
243
- end
244
- ret
245
- else
246
- val
247
- end
248
- end
249
-
250
- def ensure_has_atrts(array)
251
- unless @has_atrts
252
- array << "atrt_ids = {}\n"
253
- @has_atrts = true
254
- end
14
+ ::Brick::SeedsBuilder.generate_seeds
255
15
  end
256
16
  end
257
17
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: brick
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.228
4
+ version: 1.0.230
5
5
  platform: ruby
6
6
  authors:
7
7
  - Lorin Thwaits
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-01-19 00:00:00.000000000 Z
11
+ date: 2025-03-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -228,13 +228,17 @@ files:
228
228
  - lib/brick/util.rb
229
229
  - lib/brick/version_number.rb
230
230
  - lib/generators/brick/USAGE
231
+ - lib/generators/brick/airtable_api_caller.rb
232
+ - lib/generators/brick/airtable_migrations_generator.rb
233
+ - lib/generators/brick/airtable_seeds_generator.rb
231
234
  - lib/generators/brick/controllers_generator.rb
232
235
  - lib/generators/brick/install_generator.rb
233
- - lib/generators/brick/migration_builder.rb
236
+ - lib/generators/brick/migrations_builder.rb
234
237
  - lib/generators/brick/migrations_generator.rb
235
238
  - lib/generators/brick/models_generator.rb
236
239
  - lib/generators/brick/salesforce_migrations_generator.rb
237
240
  - lib/generators/brick/salesforce_schema.rb
241
+ - lib/generators/brick/seeds_builder.rb
238
242
  - lib/generators/brick/seeds_generator.rb
239
243
  - lib/generators/brick/templates/add_object_changes_to_versions.rb.erb
240
244
  - lib/generators/brick/templates/create_versions.rb.erb