brick 1.0.245 → 1.0.247

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.
@@ -9,18 +9,19 @@ module Brick::Rails::FormBuilder
9
9
  def brick_field(method, html_options = {}, val = nil, col = nil,
10
10
  bt = nil, bt_class = nil, bt_name = nil, bt_pair = nil)
11
11
  model = self.object.class
12
- col ||= model.columns_hash[method]
12
+ col ||= model.columns_hash[method.to_s]
13
13
  out = +'<table><tr><td>'
14
14
  html_options[:class] = 'dimmed' unless val
15
15
  is_revert = true
16
16
  template = instance_variable_get(:@template)
17
+ method_sym = method.to_sym
17
18
  if bt
18
19
  bt_class ||= bt[1].first.first
19
20
  bt_name ||= bt[1].map { |x| x.first.name }.join('/')
20
21
  bt_pair ||= bt[1].first
21
22
 
22
23
  html_options[:prompt] = "Select #{bt_name}"
23
- out << self.select(method.to_sym, bt[3], { value: val || '^^^brick_NULL^^^' }, html_options)
24
+ out << self.select(method_sym, bt[3], { value: val || '^^^brick_NULL^^^' }, html_options)
24
25
  bt_obj = nil
25
26
  begin
26
27
  bt_obj = bt_class&.find_by(bt_pair[1] => val)
@@ -38,8 +39,8 @@ module Brick::Rails::FormBuilder
38
39
  "<span class=\"orphan\">Orphaned ID: #{val}</span>".html_safe
39
40
  end
40
41
  out << bt_link if bt_link
41
- elsif model._brick_monetized_attributes&.include?(method)
42
- out << self.text_field(method.to_sym, html_options.merge({ value: Money.new(val.to_i).format }))
42
+ elsif model._brick_monetized_attributes&.include?(method.to_s)
43
+ out << self.text_field(method_sym, html_options.merge({ value: Money.new(val.to_i).format }))
43
44
  else
44
45
  col_type = if model.json_column?(col) || val.is_a?(Array)
45
46
  :json
@@ -61,40 +62,40 @@ module Brick::Rails::FormBuilder
61
62
  out << "<img src=\"#{url}\" title=\"#{val}\">"
62
63
  elsif model.respond_to?(:enumerized_attributes) && (opts = (attr = model.enumerized_attributes[method])&.options).present?
63
64
  enum_html_options = attr.kind_of?(Enumerize::Multiple) ? html_options.merge({ multiple: true, size: opts.length + 1 }) : html_options
64
- out << self.select(method.to_sym, [["(No #{method} chosen)", '^^^brick_NULL^^^']] + opts, { value: val || '^^^brick_NULL^^^' }, enum_html_options)
65
+ out << self.select(method_sym, [["(No #{method} chosen)", '^^^brick_NULL^^^']] + opts, { value: val || '^^^brick_NULL^^^' }, enum_html_options)
65
66
  else
66
67
  spit_out_text_field = true
67
68
  end
68
69
  elsif col_type == :enum
69
70
  if col.respond_to?(:limit) && col.limit.present?
70
- out << self.select(method.to_sym, [["(No #{method} chosen)", '^^^brick_NULL^^^']] + col.limit, { value: val.to_s || '^^^brick_NULL^^^' }, html_options)
71
+ out << self.select(method_sym, [["(No #{method} chosen)", '^^^brick_NULL^^^']] + col.limit, { value: val.to_s || '^^^brick_NULL^^^' }, html_options)
71
72
  else
72
73
  spit_out_text_field = true
73
74
  end
74
75
  else
75
76
  template.instance_variable_set(:@_text_fields_present, true)
76
- out << self.hidden_field(method.to_sym, html_options)
77
+ out << self.hidden_field(method_sym, html_options)
77
78
  out << "<trix-editor input=\"#{self.field_id(method)}\"></trix-editor>"
78
79
  end
79
80
  if spit_out_text_field
80
81
  # %%% Need to update the max-width with javascript when page width is adjusted?
81
82
  html_options.merge!(style: 'min-width: 154px;field-sizing: content;') # max-width: auto;
82
- out << self.text_field(method.to_sym, html_options)
83
+ out << self.text_field(method_sym, html_options)
83
84
  end
84
85
  when :boolean
85
- out << self.check_box(method.to_sym)
86
+ out << self.check_box(method_sym)
86
87
  when :integer, :decimal, :float
87
- if model.respond_to?(:attribute_types) && (enum_type = model.attribute_types[method]).is_a?(ActiveRecord::Enum::EnumType)
88
+ if model.respond_to?(:attribute_types) && (enum_type = model.attribute_types[method.to_s]).is_a?(ActiveRecord::Enum::EnumType)
88
89
  opts = enum_type.send(:mapping)&.each_with_object([]) { |v, s| s << [v.first, v.first] } || []
89
- out << self.select(method.to_sym, [["(No #{method} chosen)", '^^^brick_NULL^^^']] + opts, { value: val || '^^^brick_NULL^^^' }, options)
90
+ out << self.select(method_sym, [["(No #{method} chosen)", '^^^brick_NULL^^^']] + opts, { value: val || '^^^brick_NULL^^^' }, options)
90
91
  else
91
92
  digit_pattern = col_type == :integer ? '(?:-|)\d*' : '(?:-|)\d*(?:\.\d*|)'
92
- # Used to do this for float / decimal: self.number_field method.to_sym
93
- out << self.text_field(method.to_sym, { pattern: digit_pattern, class: 'check-validity' })
93
+ # Used to do this for float / decimal: self.number_field method_sym
94
+ out << self.text_field(method_sym, { pattern: digit_pattern, class: 'check-validity' })
94
95
  end
95
96
  when *DT_PICKERS.keys
96
97
  template.instance_variable_set(:@_date_fields_present, true)
97
- out << self.text_field(method.to_sym, { class: DT_PICKERS[col_type] })
98
+ out << self.text_field(method_sym, { class: DT_PICKERS[col_type] })
98
99
  when :uuid
99
100
  is_revert = false
100
101
  # Postgres naturally uses the +uuid_generate_v4()+ function from the uuid-ossp extension
@@ -146,7 +147,7 @@ module Brick::Rails::FormBuilder
146
147
  end
147
148
  out << self.hidden_field("_brick_attached_#{method}", value: existing.join(',')) unless existing.blank?
148
149
  # Render a "Choose File(s)" input element
149
- args = [method.to_sym]
150
+ args = [method_sym]
150
151
  args << { multiple: true } if col&.type == :files
151
152
  out << self.file_field(*args)
152
153
  when :primary_key
@@ -163,7 +164,7 @@ module Brick::Rails::FormBuilder
163
164
  end
164
165
  # Because there are so danged many quotes in JSON, escape them specially by converting to backticks.
165
166
  # (and previous to this, escape backticks with our own goofy code of ^^br_btick__ )
166
- out << (json_field = self.hidden_field(method.to_sym, { class: 'jsonpicker', value: val_str&.gsub('`', '^^br_btick__')&.tr('\"', '`')&.html_safe }))
167
+ out << (json_field = self.hidden_field(method_sym, { class: 'jsonpicker', value: val_str&.gsub('`', '^^br_btick__')&.tr('\"', '`')&.html_safe }))
167
168
  out << "<div id=\"_br_json_#{self.field_id(method)}\"></div>"
168
169
  else
169
170
  is_revert = false
@@ -177,6 +178,7 @@ module Brick::Rails::FormBuilder
177
178
  end
178
179
  out << "</td></tr></table>
179
180
  "
181
+ @_brick&.fetch(:fields_rendered, nil)&.<< method.to_s # Track that we've rendered this one
180
182
  out.html_safe
181
183
  end # brick_field
182
184
  end
@@ -30,7 +30,7 @@ module Brick::Rails::FormTags
30
30
  col_keys = relation.columns.each_with_object([]) do |col, s|
31
31
  col_name = col.name
32
32
  next if inclusions&.exclude?(col_name) ||
33
- (pk.include?(col_name) && [:integer, :uuid].include?(col.type) && !bts&.key?(col_name)) ||
33
+ (pk.include?(col_name) && ([:string, :text].exclude?(col.type) || col_name == 'airtable_id') && !bts&.key?(col_name)) ||
34
34
  ::Brick.config.metadata_columns.include?(col_name) || poly_cols&.include?(col_name)
35
35
 
36
36
  s << col_name
@@ -112,7 +112,7 @@ module Brick::Rails::FormTags
112
112
  next unless klass.table_exists?
113
113
 
114
114
  rid = pk.map { |pk_part| obj.send(pk_part.to_sym) }
115
- out << "<tr x-id=\"#{rid.join('/')}\">\n"
115
+ out << "<tr x-id=\"#{slashed_rid = rid.join('/')}\">\n"
116
116
  out << "<td class=\"col-sticky alternating-gray\">#{link_to('⇛', send("#{klass._brick_index(:singular)}_path".to_sym, rid),
117
117
  { class: 'big-arrow' })}</td>\n" if pk.present?
118
118
  ac = obj.instance_variable_get(:@association_cache) || obj.instance_variable_set(:@association_cache, {})
@@ -134,8 +134,14 @@ module Brick::Rails::FormTags
134
134
  if bt[2] && obj.respond_to?(poly_id_col = "#{bt.first}_id") # Polymorphic?
135
135
  if (poly_id = obj.send(poly_id_col))
136
136
  bt_class = obj.send(klass.brick_foreign_type(bt.first))
137
- base_class_underscored = (::Brick.existing_stis[bt_class] || bt_class).constantize.base_class._brick_index(:singular)
138
- out << link_to("#{bt_class} ##{poly_id}", send("#{base_class_underscored}_path".to_sym, poly_id))
137
+ root_class_name = (::Brick.existing_stis[bt_class] || bt_class)
138
+ begin
139
+ base_class_underscored = root_class_name.constantize.base_class._brick_index(:singular)
140
+ out << link_to("#{bt_class} ##{poly_id}", send("#{base_class_underscored}_path".to_sym, poly_id))
141
+ rescue NameError => ee
142
+ puts "WARNING: While searching for a polymorphically associated #{bt.first} for the #{klass.name} with ID #{slashed_rid}, was unable to locate a model called \"#{root_class_name}\". This model might have been deleted, thereby orphaning rows in the #{klass.table_name} table."
143
+ out << "<span class=\"orphan\">&lt;&lt; Orphaned \"#{root_class_name}\" with ID: #{slashed_rid} >></span>"
144
+ end
139
145
  end
140
146
  else # BT or HOT
141
147
  bt_class = bt[1].first.first
@@ -151,11 +157,17 @@ module Brick::Rails::FormTags
151
157
  end
152
158
  br_descrip_args = [obj]
153
159
  br_descrip_args += [bt_class._brick_deserialized_value(obj, descrips[0..-2]), bt_id_col] if descrips
154
- bt_txt = bt_class.brick_descrip(*br_descrip_args)
155
- bt_txt = ::Brick::Rails.display_binary(bt_txt).html_safe if bt_txt&.encoding&.name == 'ASCII-8BIT'
156
- bt_txt ||= "<span class=\"orphan\">&lt;&lt; Orphaned ID: #{val} >></span>" if val
157
- bt_id = bt_class._brick_deserialized_id(obj, bt_id_col)
158
- out << (bt_id&.first ? link_to(bt_txt, send("#{bt_class.base_class._brick_index(:singular)}_path".to_sym, bt_id)) : bt_txt || '')
160
+ out << if bt_id_col.blank? && bt[1].first[1] # Is this a BT which references a table with no primary key?
161
+ # Although we have no way to directly reference this record via a show action, we can at least create a link like this:
162
+ # http://localhost:3000/brick/local_authority_postcode?__value=PA2Y+5LQ
163
+ link_to(val, send("#{bt_class.base_class._brick_index(:singular)}_path".to_sym, '__the_id__')).gsub('__the_id__', "?__#{bt[1].first[1]}=#{CGI.escape(val)}")
164
+ else # Normal BT or HOT
165
+ bt_txt = bt_class.brick_descrip(*br_descrip_args)
166
+ bt_txt = ::Brick::Rails.display_binary(bt_txt).html_safe if bt_txt&.encoding&.name == 'ASCII-8BIT'
167
+ bt_txt ||= "<span class=\"orphan\">&lt;&lt; Orphaned ID: #{val} >></span>" if val
168
+ bt_id = bt_class._brick_deserialized_id(obj, bt_id_col)
169
+ (bt_id&.first ? link_to(bt_txt, send("#{bt_class.base_class._brick_index(:singular)}_path".to_sym, bt_id)) : bt_txt || '')
170
+ end
159
171
  end
160
172
  elsif (hms_col = hms_cols[col_name])
161
173
  if hms_col.length == 1
@@ -165,15 +177,14 @@ module Brick::Rails::FormTags
165
177
  if col[2] == 'HO'
166
178
  descrips = bt_descrip[col_name.to_sym][hm_klass]
167
179
  if (descrip_parts = hm_klass._brick_deserialized_value(obj, descrips[0..-2]))&.first
180
+ ho_id = hm_klass._brick_deserialized_id(obj, ho_id_col = descrips.last)
168
181
  ho_txt = if hm_klass.name == 'ActiveStorage::Attachment'
169
182
  begin
170
183
  ::Brick::Rails.display_binary(obj.send(col[3])&.blob&.download)&.html_safe
171
184
  rescue
185
+ # Attachment is missing for some reason ... default to showing just a description.
172
186
  end
173
- else
174
- hm_klass.brick_descrip(obj, descrip_parts, ho_id_col = descrips.last)
175
- end
176
- ho_id = hm_klass._brick_deserialized_id(obj, ho_id_col)
187
+ end || hm_klass.brick_descrip(obj, descrip_parts, ho_id_col)
177
188
  out << link_to(ho_txt, send("#{hm_klass.base_class._brick_index(:singular)}_path".to_sym, ho_id))
178
189
  end
179
190
  elsif obj.respond_to?(ct_col = hms_col[1].to_sym) && (ct = obj.send(ct_col)&.to_i)&.positive?
@@ -241,25 +252,52 @@ module Brick::Rails::FormTags
241
252
 
242
253
  # -----------------------------
243
254
  # Our mega show/new/update form
244
- def brick_form_for(obj, options = {}, model = obj.class, bts = {}, pk = (obj.class.primary_key || []))
255
+ def brick_form_with(**options, &block)
256
+ obj = options.delete(:model) || instance_variable_get("@#{obj_name = controller_name.singularize}".to_sym)
257
+ unless obj
258
+ puts "WARNING: Could not find #{obj_name} object"
259
+ return
260
+ end
261
+
262
+ model = obj.class
263
+ bts = options.delete(:bts) || {}
264
+ pk = options.delete(:pk) || model.primary_key || []
245
265
  pk = [pk] unless pk.is_a?(Array)
246
266
  pk.map!(&:to_s)
267
+ brick_stuff = { fields_rendered: (fields_rendered = []), bts: bts, pk: pk }
268
+
269
+ # options[:model] = obj.becomes(model.base_class) unless options.include?(:model)
270
+ unless options.include?(:url)
271
+ options[:url] = if obj.new_record?
272
+ link_to_brick(obj.class, path_only: true) # Properly supports STI, but only works for :new
273
+ else
274
+ path_helper = obj.new_record? ? model._brick_index : model._brick_index(:singular)
275
+ send("#{path_helper}_path".to_sym, obj) if ::Brick.config.path_prefix || (path_helper != obj.class.table_name)
276
+ end
277
+ end
278
+
279
+ # We call into the older #form_for in order to have compatibility with Rails < 5.1
247
280
  form_for(obj.becomes(model.base_class), options) do |f|
248
- out = +'<table class="shadow">'
249
- has_fields = false
281
+ out = +''
282
+ out << '<table class="shadow">'
283
+ f.instance_variable_set(:@_brick, brick_stuff)
284
+ has_fields = has_updatable_fields = nil
250
285
  # If it's a new record, set any default polymorphic types
251
- bts&.each do |_k, v|
286
+ bts.each do |_k, v|
252
287
  if v[2]
253
288
  obj.send("#{model.brick_foreign_type(v.first)}=", v[1].first&.first&.name)
254
289
  end
255
290
  end if obj.new_record?
256
291
  hoa, hma, rtans = model._activestorage_actiontext_fields
257
- (model.column_names + hoa + hma + rtans.keys).each do |k|
292
+
293
+ block.call(f) if block_given?
294
+
295
+ ((model.column_names + hoa + hma + rtans.keys) - fields_rendered).each do |k|
258
296
  pk_pos = (pk.index(k)&.+ 1)
259
- next if (pk_pos && pk.length == 1 && !bts.key?(k)) ||
297
+ col = model.columns_hash[k]
298
+ next if (pk_pos && pk.length == 1 && !bts.key?(k) && [:string, :text].exclude?(col.type)) ||
260
299
  ::Brick.config.metadata_columns.include?(k)
261
300
 
262
- col = model.columns_hash[k]
263
301
  if !col
264
302
  kwargs = if hoa.include?(k) # has_one_attached
265
303
  { sql_type: 'binary', type: :file }
@@ -280,7 +318,7 @@ module Brick::Rails::FormTags
280
318
  val = obj.attributes[k]
281
319
  out << "
282
320
  <tr>
283
- <th class=\"show-field\"#{" title=\"#{col&.comment}\"".html_safe if col&.respond_to?(:comment) && !col&.comment.blank?}>"
321
+ <th class=\"show-field\"#{" title=\"#{col&.comment}\"" if col&.respond_to?(:comment) && !col&.comment.blank?}>"
284
322
  has_fields = true
285
323
  if (bt = bts[k])
286
324
  # Add a final member in this array with descriptive options to be used in <select> drop-downs
@@ -323,35 +361,38 @@ module Brick::Rails::FormTags
323
361
  else
324
362
  out << model.human_attribute_name(k, { default: k })
325
363
  end
326
- out << " (PK #{pk_pos})" if pk_pos
364
+ out << " (PK&nbsp;#{pk_pos})" if pk_pos
327
365
  out << "
328
366
  </th>
329
367
  <td>
330
368
  "
331
- if pk_pos
369
+ if pk_pos &&
370
+ # Allow new records to have a text-based primary key to be entered
371
+ (!obj.new_record? || [:string, :text].exclude?(col.type))
332
372
  out << val.to_s
333
373
  else
374
+ has_updatable_fields = true
334
375
  out << f.brick_field(k, html_options = {}, val, col, bt, bt_class, bt_name, bt_pair)
335
376
  end
336
377
  out << "
337
378
  </td>
338
379
  </tr>"
339
380
  end
340
- if has_fields
381
+ if has_updatable_fields
341
382
  out << "<tr><td colspan=\"2\">#{f.submit({ class: 'update' })}</td></tr>"
342
- else
383
+ elsif !has_fields
343
384
  out << '<tr><td colspan="2">(No displayable fields)</td></tr>'
344
385
  end
345
386
  out << '</table>'
346
387
  if model.name == 'ActiveStorage::Attachment'
347
388
  begin
348
- out << ::Brick::Rails.display_binary(obj&.blob&.download, 500_000)&.html_safe
389
+ out << ::Brick::Rails.display_binary(obj&.blob&.download, 500_000)
349
390
  rescue
350
391
  end
351
392
  end
352
- out.html_safe
353
- end
354
- end # brick_form_for
393
+ output_buffer << out.html_safe
394
+ end # form_for
395
+ end # brick_form_with
355
396
 
356
397
  # ------------------------------------------
357
398
  # Our cool N:M checkbox constellation editor
@@ -402,7 +443,7 @@ module Brick::Rails::FormTags
402
443
  end
403
444
  out << " </tbody>
404
445
  </table>
405
- <script<%= @_request.env['_brick_nonce'] %>>
446
+ <script#{@_request.env['_brick_nonce']}>
406
447
  var constellation = document.getElementById(\"#{table_name}\");
407
448
  var nextSib,
408
449
  _this;
@@ -469,7 +510,6 @@ module Brick::Rails::FormTags
469
510
  out << " </tbody>
470
511
  </table>
471
512
  "
472
-
473
513
  out.html_safe
474
514
  end # brick_bezier
475
515
 
@@ -526,7 +566,7 @@ module Brick::Rails::FormTags
526
566
  </div>
527
567
  </div>
528
568
  "
529
- out
569
+ out.html_safe
530
570
  end # brick_header
531
571
 
532
572
  # All the standard CSS with teal colouration for use with Brick
@@ -536,6 +576,8 @@ module Brick::Rails::FormTags
536
576
 
537
577
  # -----------------------------------------------------------------------------------------------
538
578
  def set_grid_javascript(klass, pk, show_new_button = nil, row_count = nil, total_row_count = nil)
579
+ camel_name = klass.name.split('::').last
580
+ camel_name[0].downcase!
539
581
  table_name = klass.table_name.split('.').last
540
582
 
541
583
  # Javascript for brick_grid and brick_constellation
@@ -545,13 +587,13 @@ module Brick::Rails::FormTags
545
587
  // Plunk the row count in now that we know it
546
588
  var rowCount = document.getElementById(\"rowCount\");
547
589
  if (rowCount) rowCount.innerHTML = \"#{pluralize(row_count, "row")}#{total_row_count} &nbsp;\";
548
- var #{table_name}HtColumns;
590
+ var #{camel_name}HtColumns;
549
591
  " unless row_count.nil?
550
592
 
551
593
  grid_scripts << "
552
594
  // Snag first TR for sticky header
553
595
  var grid = document.getElementById(\"#{table_name}\");
554
- #{table_name}HtColumns = grid && [grid.getElementsByTagName(\"TR\")[0]];
596
+ #{camel_name}HtColumns = grid && [grid.getElementsByTagName(\"TR\")[0]];
555
597
  var headerTop = document.getElementById(\"headerTop\");
556
598
  var headerCols;
557
599
  if (grid) {
@@ -604,8 +646,8 @@ function setHeaderSizes() {
604
646
 
605
647
  // Set up proper sizings of sticky column header
606
648
  var node;
607
- for (var j = 0; j < #{table_name}HtColumns.length; ++j) {
608
- var row = #{table_name}HtColumns[j];
649
+ for (var j = 0; j < #{camel_name}HtColumns.length; ++j) {
650
+ var row = #{camel_name}HtColumns[j];
609
651
  var tr = isEmpty ? document.createElement(\"TR\") : headerTop.childNodes[j];
610
652
  tr.innerHTML = row.innerHTML.trim();
611
653
  var curLeft = 0.0;
data/lib/brick/rails.rb CHANGED
@@ -6,7 +6,7 @@ require 'brick/rails/engine'
6
6
  module ::Brick::Rails
7
7
  class << self
8
8
  # Low-level way to render read-only data for a field based on its data type.
9
- # Used by both brick_grid and brick_form_for (which gets down to this low-level
9
+ # Used by both brick_grid and brick_form_with (which gets down to this low-level
10
10
  # implementation from brick_field).
11
11
  def display_value(col_type, val, lat_lng = nil)
12
12
  is_mssql_geography = nil
@@ -179,7 +179,7 @@ erDiagram
179
179
  'timestamp without time zone' => 'timestamp',
180
180
  'timestamp with time zone' => 'timestamp',
181
181
  'time without time zone' => 'time',
182
- 'time with time zone' => 'time' }[dt] || dt&.tr(' (', '_').tr(')', '') || 'int'
182
+ 'time with time zone' => 'time' }[dt] || dt&.tr(' (', '_')&.tr(')', '') || 'int'
183
183
  end
184
184
  callbacks.merge({#{model_short_name.inspect} => ::#{model.name}}).each do |cb_k, cb_class|
185
185
  cb_relation = ::Brick.relations[cb_class.table_name]
@@ -143,7 +143,7 @@ module Brick
143
143
  ActiveRecord::Base.execute_sql("SET SEARCH_PATH = ?", schema)
144
144
  else
145
145
  puts "*** In the brick.rb initializer the line \"::Brick.schema_behavior = ...\" refers to schema(s) called #{possible_schemas.map { |s| "\"#{s}\"" }.join(', ')}. No mentioned schema exists. ***"
146
- if ::Brick.db_schemas.key?(::Brick.apartment_default_tenant)
146
+ if Object.const_defined?('Apartment') && ::Brick.db_schemas.key?(::Brick.apartment_default_tenant)
147
147
  ::Brick.default_schema = schema = ::Brick.apartment_default_tenant
148
148
  end
149
149
  end
@@ -214,6 +214,7 @@ module Brick
214
214
  cols[col_name] = [r['data_type'], r['max_length'], measures&.include?(col_name), r['is_nullable'] == 'NO']
215
215
  # puts "KEY! #{r['relation_name']}.#{col_name} #{r['key']} #{r['const']}" if r['key']
216
216
  relation[:col_descrips][col_name] = r['column_description'] if r['column_description']
217
+ relation[:col_order] << col_name
217
218
  end
218
219
  else # MySQL2, OracleEnhanced, and MSSQL act a little differently, bringing back an array for each row
219
220
  schema_and_tables = case ActiveRecord::Base.connection.adapter_name
@@ -269,6 +270,7 @@ ORDER BY 1, 2, c.internal_column_id, acc.position"
269
270
  # 'data_type', 'max_length', measure, 'is_nullable'
270
271
  cols[col_name] = [r[4], r[5], measures&.include?(col_name), r[8] == 'NO']
271
272
  # puts "KEY! #{r['relation_name']}.#{col_name} #{r['key']} #{r['const']}" if r['key']
273
+ relation[:col_order] << col_name
272
274
  end
273
275
  end
274
276
 
@@ -425,8 +427,7 @@ ORDER BY 1, 2, c.internal_column_id, acc.position"
425
427
  end
426
428
  )&.present?
427
429
  if tnp.last&.start_with?('::') # TNP that points to an exact class?
428
- # Had considered: [tnp.last[2..-1]]
429
- [singular]
430
+ tnp.last[2..-1].split('::')
430
431
  elsif tnp.last
431
432
  v[:auto_prefixed_schema], v[:auto_prefixed_class] = tnp
432
433
  # v[:resource] = rel_name.last[tnp.first.length..-1]
@@ -444,14 +445,18 @@ ORDER BY 1, 2, c.internal_column_id, acc.position"
444
445
  loop do
445
446
  klass = Object
446
447
  proposed_name_parts.each do |part|
447
- if klass.const_defined?(part) && klass.name != part
448
- begin
448
+ begin
449
+ if klass.const_defined?(part) && klass.name != part
449
450
  klass = klass.const_get(part)
450
- rescue NoMethodError => e
451
+ else
451
452
  klass = nil
452
453
  break
453
454
  end
454
- else
455
+ rescue NoMethodError => e
456
+ klass = nil
457
+ break
458
+ rescue NameError => e
459
+ puts "WARNING: Unable to automatically generate a class for the table named #{part}.\n In the brick.rb initializer file you can add the line \"::Brick.table_name_prefixes = { '#{part}' => '::YourModelName' }\" in order to specify a usable class name for this object."
455
460
  klass = nil
456
461
  break
457
462
  end
@@ -5,7 +5,7 @@ module Brick
5
5
  module VERSION
6
6
  MAJOR = 1
7
7
  MINOR = 0
8
- TINY = 245
8
+ TINY = 247
9
9
 
10
10
  # PRE is nil unless it's a pre-release (beta, RC, etc.)
11
11
  PRE = nil
data/lib/brick.rb CHANGED
@@ -162,7 +162,7 @@ module Brick
162
162
  (existing = @relations.find { |_k, v| v.fetch(:db_name, nil) == db_name })
163
163
  existing.last
164
164
  else
165
- Hash.new { |h, k| h[k] = Hash.new { |h, k| h[k] = {} } }.tap do |h|
165
+ Hash.new { |h, k| h[k] = Hash.new { |h, k| h[k] = (k == :col_order ? [] : {}) } }.tap do |h|
166
166
  h[:db_name] = db_name if db_name
167
167
  end
168
168
  end
@@ -5,7 +5,7 @@ module Brick
5
5
  class << self
6
6
  include FancyGets
7
7
 
8
- def pick_tables(usage = :migrations)
8
+ def pick_tables(is_airtable_id, usage = :migrations)
9
9
  puts "In order to reference Airtable data you will need a Personal Access Token (PAT) which can be generated by referencing this URL:
10
10
  https://airtable.com/create/tokens
11
11
  You need only #{usage == :migrations ? 'this scope:' : "these three scopes:
@@ -19,6 +19,10 @@ Please provide your Airtable PAT:"
19
19
  require 'net/http'
20
20
  # Generate a list of bases that can be chosen
21
21
  bases = https_get('https://api.airtable.com/v0/meta/bases', pat)
22
+ if (error = bases.fetch('error', nil))
23
+ puts error.fetch('message', nil) || 'Error'
24
+ return
25
+ end
22
26
  base = gets_list(bases.fetch('bases', nil)&.map { |z| AirtableTable.new(z['id'], z['name']) })
23
27
  puts
24
28
  # Generate a list of tables that can be chosen
@@ -35,10 +39,11 @@ Please provide your Airtable PAT:"
35
39
  # Build out a '::Brick.relations' hash that represents this Airtable schema
36
40
  fks = []
37
41
  associatives = {}
38
- relations = chosen.each_with_object({}) do |table, s|
42
+ relations = chosen.each_with_object({base_name: base.name}) do |table, s|
39
43
  tbl_name = sane_table_name(table.name)
40
44
  # Build out columns and foreign keys
41
45
  cols = {}
46
+ col_order = []
42
47
  table.fields.each do |col|
43
48
  col_name = sane_name(col['name'])
44
49
  # This is like a has_many or has_many through
@@ -48,7 +53,8 @@ Please provide your Airtable PAT:"
48
53
  chosen.find { |t| t.id == col['options']['linkedTableId'] }&.name
49
54
  ))
50
55
  if col['options']['prefersSingleRecordLink'] # 1:M
51
- fks << [frn_tbl, "#{col_name}_id", tbl_name, col_name]
56
+ fks << [frn_tbl, (fk_col_name = "#{col_name}_id"), tbl_name, col_name]
57
+ col_order << fk_col_name
52
58
  else # N:M
53
59
  # Queue up to build associative table with two foreign keys
54
60
  camelized = (assoc_name = "#{tbl_name}_#{col_name}_#{frn_tbl}").camelize
@@ -64,17 +70,17 @@ Please provide your Airtable PAT:"
64
70
  else
65
71
  # puts col['type']
66
72
  dt = case col['type']
67
- when 'singleLineText', 'url', 'email', 'singleSelect'
73
+ when 'singleLineText', 'url', 'email', 'singleSelect', 'phoneNumber'
68
74
  'string'
69
75
  when 'multilineText'
70
76
  'text'
71
77
  when 'number', 'currency'
72
- 'decimal'
78
+ col['options']['precision'] == 0 ? 'integer' : 'decimal'
73
79
  when 'checkbox'
74
80
  'boolean'
75
81
  when 'date'
76
82
  'date'
77
- when 'multipleSelects'
83
+ when 'multipleSelects', 'singleCollaborator'
78
84
  # Sqlite3 can do json
79
85
  'json'
80
86
  when 'formula', 'count', 'rollup', 'multipleAttachments'
@@ -83,13 +89,21 @@ Please provide your Airtable PAT:"
83
89
  # binding.pry
84
90
  end
85
91
  cols[col_name] = [dt, nil, true, false] # true is the col[:nillable]
92
+ col_order << col_name
86
93
  end
87
94
  end
88
95
  # Put it all into a relation entry, named the same as the table
89
- pkey = table.fields.find { |f| f['id'] == table.primary_key }['name']
96
+ pkey = if is_airtable_id
97
+ cols['airtable_id'] = ['string', nil, true, false]
98
+ 'airtable_id'
99
+ else
100
+ table.fields.find { |f| f['id'] == table.primary_key }['name']
101
+ end
102
+
90
103
  s[tbl_name] = {
91
104
  pkey: { "#{tbl_name}_pkey" => [sane_name(pkey)] },
92
105
  cols: cols,
106
+ col_order: col_order,
93
107
  fks: {},
94
108
  airtable_table: table
95
109
  }
@@ -15,9 +15,10 @@ module Brick
15
15
  def airtable_migrations
16
16
  mig_path, is_insert_versions, is_delete_versions = ::Brick::MigrationsBuilder.check_folder
17
17
  return unless mig_path &&
18
- (relations = ::Brick::AirtableApiCaller.pick_tables)
18
+ (relations = ::Brick::AirtableApiCaller.pick_tables(!ENV['OMIT_AIRTABLE_ID']))
19
19
 
20
- ::Brick::MigrationsBuilder.generate_migrations(relations.keys, mig_path, is_insert_versions, is_delete_versions, relations,
20
+ chosen = relations.keys.reject { |k| k.is_a?(Symbol) }
21
+ ::Brick::MigrationsBuilder.generate_migrations(chosen, mig_path, is_insert_versions, is_delete_versions, relations,
21
22
  do_fks_last: 'Separate', do_schema_migrations: false)
22
23
  end
23
24
  end
@@ -11,7 +11,7 @@ module Brick
11
11
  desc 'Auto-generates a seeds file from existing data in an Airtable "base".'
12
12
 
13
13
  def airtable_seeds
14
- return unless (relations = ::Brick::AirtableApiCaller.pick_tables(:seeds))
14
+ return unless (relations = ::Brick::AirtableApiCaller.pick_tables(!ENV['OMIT_AIRTABLE_ID'], :seeds))
15
15
 
16
16
  ::Brick::SeedsBuilder.generate_seeds(relations)
17
17
  end
@@ -43,7 +43,7 @@ module Brick
43
43
  relation.last[:cols].each do |col, type|
44
44
  col_down = col.downcase
45
45
 
46
- if (is_possible_poly = ['character varying', 'text'].include?(type.first))
46
+ if (is_possible_poly = ['character varying', 'varchar', 'text', 'VARCHAR2'].include?(type.first))
47
47
  if col_down.end_with?('_type')
48
48
  poly_type_cut_length = -6
49
49
  col_down = col_down[0..-6]