brick 1.0.246 → 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.
- checksums.yaml +4 -4
- data/lib/brick/extensions.rb +65 -41
- data/lib/brick/rails/engine.rb +6 -13
- data/lib/brick/rails/form_builder.rb +18 -16
- data/lib/brick/rails/form_tags.rb +55 -24
- data/lib/brick/rails.rb +1 -1
- data/lib/brick/reflect_tables.rb +11 -5
- data/lib/brick/version_number.rb +1 -1
- data/lib/brick.rb +1 -1
- data/lib/generators/brick/airtable_api_caller.rb +21 -7
- data/lib/generators/brick/airtable_migrations_generator.rb +3 -2
- data/lib/generators/brick/airtable_seeds_generator.rb +1 -1
- data/lib/generators/brick/install_generator.rb +1 -1
- data/lib/generators/brick/migrations_builder.rb +25 -12
- data/lib/generators/brick/seeds_builder.rb +75 -33
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: baf36636026c4ecad5ff76ccdeea80604c6c9128f7065d7b546ab41cc4fccf1c
|
|
4
|
+
data.tar.gz: ec8c081354229eb5b4cdcd3112b24313468aaf731f856168cace28d3882e1f51
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3742d4d628c4f0ff18008f5dd965b3e49c545d24141cea917a0ba141cff7f3243b6f159e732f2e786b87f274e80237bcea2af74f17b9494345cf898e54e72928
|
|
7
|
+
data.tar.gz: 3aa5e54bc3b4af270e5b2342430e294920b151139af66ec5771b15a575377f7275f9c10e593d7c1d314e67d145fd9ffb0ae834b4f68a5d4c1afff8b22ecc404c
|
data/lib/brick/extensions.rb
CHANGED
|
@@ -105,13 +105,41 @@ module ActiveRecord
|
|
|
105
105
|
col_names = columns_hash.keys
|
|
106
106
|
# If it's a composite primary key then allow all the values through
|
|
107
107
|
# TODO: Should disallow any autoincrement / SERIAL columns
|
|
108
|
-
if skip_id && (pk_as_array = _pk_as_array).length == 1
|
|
109
|
-
|
|
108
|
+
if skip_id && (pk_as_array = _pk_as_array).length == 1 &&
|
|
109
|
+
[:string, :text].exclude?(columns_hash[pk_as_array.first]&.type)
|
|
110
|
+
col_names -= pk_as_array
|
|
110
111
|
end
|
|
111
112
|
hoa, hma, rtans = _activestorage_actiontext_fields
|
|
112
113
|
col_names.map(&:to_sym) + hoa + hma.map { |as| { as => [] } } + rtans.values
|
|
113
114
|
end
|
|
114
115
|
|
|
116
|
+
def _brick_set_wheres(params, join_array = nil)
|
|
117
|
+
is_distinct = nil
|
|
118
|
+
wheres = {}
|
|
119
|
+
params.each do |k, v|
|
|
120
|
+
k = k.to_s # Rails < 4.2 comes in as a symbol
|
|
121
|
+
next unless k.start_with?('__')
|
|
122
|
+
|
|
123
|
+
k = k[2..-1] # Take off leading "__"
|
|
124
|
+
if (where_col = (ks = k.split('.')).last)[-1] == '!'
|
|
125
|
+
where_col = where_col[0..-2]
|
|
126
|
+
end
|
|
127
|
+
case ks.length
|
|
128
|
+
when 1
|
|
129
|
+
next unless self.column_names.any?(where_col) || self._brick_get_fks.include?(where_col)
|
|
130
|
+
when 2
|
|
131
|
+
assoc_name = ks.first.to_sym
|
|
132
|
+
# Make sure it's a good association name and that the model has that column name
|
|
133
|
+
next unless self.reflect_on_association(assoc_name)&.klass&.column_names&.any?(where_col)
|
|
134
|
+
|
|
135
|
+
join_array&.[]=(assoc_name, nil) # Store this relation name in our special collection for .joins()
|
|
136
|
+
is_distinct = true
|
|
137
|
+
end
|
|
138
|
+
wheres[k] = v.is_a?(String) ? v.split(',') : v
|
|
139
|
+
end
|
|
140
|
+
[wheres, is_distinct]
|
|
141
|
+
end
|
|
142
|
+
|
|
115
143
|
# Return three lists of fields for this model --
|
|
116
144
|
# has_one_attached, has_many_attached, and has_rich_text
|
|
117
145
|
def _activestorage_actiontext_fields
|
|
@@ -219,10 +247,17 @@ module ActiveRecord
|
|
|
219
247
|
else
|
|
220
248
|
# If there's no DSL yet specified, just try to find the first usable column on this model
|
|
221
249
|
dsl = if table_exists?
|
|
222
|
-
skip_columns = _brick_get_fks + (::Brick.config.metadata_columns || [])
|
|
223
|
-
|
|
250
|
+
skip_columns = _brick_get_fks + (::Brick.config.metadata_columns || [])
|
|
251
|
+
column_list = columns.dup
|
|
252
|
+
pk_idx = column_list.index { |c| c.name == primary_key }
|
|
253
|
+
if pk_idx
|
|
254
|
+
pk_column = column_list.delete_at(pk_idx)
|
|
255
|
+
# if the PK column is a string, put it at the end so it will get chosen as a last resort
|
|
256
|
+
column_list << pk_column if [:string, :text].include?(columns_hash[primary_key]&.type)
|
|
257
|
+
end
|
|
258
|
+
if (descrip_col = column_list.find { |c| c.type == :string && skip_columns.exclude?(c.name) })
|
|
224
259
|
"[#{descrip_col.name}]"
|
|
225
|
-
elsif (descrip_col =
|
|
260
|
+
elsif (descrip_col = column_list.find { |c| [:boolean, :binary, :xml].exclude?(c.type) && skip_columns.exclude?(c.name) })
|
|
226
261
|
"[#{descrip_col.name}]"
|
|
227
262
|
else
|
|
228
263
|
"#{name} ##{_pk_as_array.map { |pk_part| "[#{pk_part}]" }.join(', ')}"
|
|
@@ -689,32 +724,10 @@ module ActiveRecord
|
|
|
689
724
|
# model early in case the user wants to do an ORDER BY based on any of that.
|
|
690
725
|
model._brick_calculate_bts_hms(translations, join_array) if is_add_bts || is_add_hms
|
|
691
726
|
|
|
692
|
-
is_distinct = nil
|
|
693
|
-
wheres = {}
|
|
694
727
|
params = params.to_unsafe_h unless params.is_a?(Hash)
|
|
695
728
|
params.merge!(args[1]) if args[1]
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
next unless k.start_with?('__')
|
|
699
|
-
|
|
700
|
-
k = k[2..-1] # Take off leading "__"
|
|
701
|
-
if (where_col = (ks = k.split('.')).last)[-1] == '!'
|
|
702
|
-
where_col = where_col[0..-2]
|
|
703
|
-
end
|
|
704
|
-
case ks.length
|
|
705
|
-
when 1
|
|
706
|
-
next unless klass.column_names.any?(where_col) || klass._brick_get_fks.include?(where_col)
|
|
707
|
-
when 2
|
|
708
|
-
assoc_name = ks.first.to_sym
|
|
709
|
-
# Make sure it's a good association name and that the model has that column name
|
|
710
|
-
next unless klass.reflect_on_association(assoc_name)&.klass&.column_names&.any?(where_col)
|
|
711
|
-
|
|
712
|
-
join_array[assoc_name] = nil # Store this relation name in our special collection for .joins()
|
|
713
|
-
is_distinct = true
|
|
714
|
-
distinct!
|
|
715
|
-
end
|
|
716
|
-
wheres[k] = v.is_a?(String) ? v.split(',') : v
|
|
717
|
-
end
|
|
729
|
+
wheres, is_distinct = klass._brick_set_wheres(params, join_array)
|
|
730
|
+
distinct! if is_distinct
|
|
718
731
|
|
|
719
732
|
# %%% Skip the metadata columns
|
|
720
733
|
if selects.empty? # Default to all columns
|
|
@@ -1756,7 +1769,7 @@ class Object
|
|
|
1756
1769
|
end
|
|
1757
1770
|
full_name = if relation || schema_name.blank?
|
|
1758
1771
|
if singular_table_name != table_name.singularize && # %%% Try this with http://localhost:3000/brick/spree/property_translations
|
|
1759
|
-
(schema_module = ::Brick.config.table_name_prefixes.find { |k, v| table_name.start_with?(k) }&.last&.constantize)
|
|
1772
|
+
(schema_module = ::Brick.config.table_name_prefixes.find { |k, v| table_name.length > k&.length && table_name.start_with?(k) }&.last&.constantize)
|
|
1760
1773
|
"#{schema_module&.name}::#{inheritable_name || model_name}"
|
|
1761
1774
|
else
|
|
1762
1775
|
inheritable_name || model_name
|
|
@@ -1775,7 +1788,7 @@ class Object
|
|
|
1775
1788
|
::Brick.config.exclude_tables.include?(matching)
|
|
1776
1789
|
|
|
1777
1790
|
# Are they trying to use a pluralised class name such as "Employees" instead of "Employee"?
|
|
1778
|
-
if table_name == singular_table_name && !ActiveSupport::Inflector.inflections.uncountable.include?(table_name)
|
|
1791
|
+
if table_name == singular_table_name && model_name.pluralize == model_name && !ActiveSupport::Inflector.inflections.uncountable.include?(table_name)
|
|
1779
1792
|
# unless ::Brick.config.sti_namespace_prefixes&.key?("::#{singular_table_name.camelize}::")
|
|
1780
1793
|
# puts "Warning: Class name for a model that references table \"#{matching
|
|
1781
1794
|
# }\" should be \"#{ActiveSupport::Inflector.singularize(inheritable_name || model_name)}\"."
|
|
@@ -2475,10 +2488,19 @@ class Object
|
|
|
2475
2488
|
|
|
2476
2489
|
real_model = model.find_real_model(params)
|
|
2477
2490
|
|
|
2491
|
+
# %%% Allow params to define which columns to use for order_by
|
|
2492
|
+
# Overriding the default by providing a querystring param?
|
|
2493
|
+
order_by = params['_brick_order']&.split(',')&.map(&:to_sym) || Object.send(:default_ordering, table_name, pk)
|
|
2494
|
+
|
|
2478
2495
|
if request.format == :csv # Asking for a template?
|
|
2479
2496
|
require 'csv'
|
|
2480
2497
|
exported_csv = CSV.generate(force_quotes: false) do |csv_out|
|
|
2481
|
-
|
|
2498
|
+
export_args = [true, real_model.brick_import_template, false]
|
|
2499
|
+
if real_model.method(:df_export).parameters.length > 3
|
|
2500
|
+
wheres, _is_distinct = real_model._brick_set_wheres(params)
|
|
2501
|
+
export_args += [wheres, order_by]
|
|
2502
|
+
end
|
|
2503
|
+
real_model.df_export(*export_args).each do |row|
|
|
2482
2504
|
row.each do |d|
|
|
2483
2505
|
row.each_with_index do |d, idx|
|
|
2484
2506
|
# "false" disallows HTML encoding the descriptions of binary content
|
|
@@ -2499,11 +2521,6 @@ class Object
|
|
|
2499
2521
|
end
|
|
2500
2522
|
|
|
2501
2523
|
# Normal (not swagger or CSV) request
|
|
2502
|
-
|
|
2503
|
-
# %%% Allow params to define which columns to use for order_by
|
|
2504
|
-
# Overriding the default by providing a querystring param?
|
|
2505
|
-
order_by = params['_brick_order']&.split(',')&.map(&:to_sym) || Object.send(:default_ordering, table_name, pk)
|
|
2506
|
-
|
|
2507
2524
|
ar_relation = ActiveRecord.version < Gem::Version.new('4') ? real_model.preload : real_model.all
|
|
2508
2525
|
params['_brick_is_api'] = true if (is_api = request.format == :js || current_api_root)
|
|
2509
2526
|
@_brick_params = ar_relation._brick_querying((selects ||= []), params: params, order_by: order_by,
|
|
@@ -2696,7 +2713,14 @@ class Object
|
|
|
2696
2713
|
render json: { result: es_result }
|
|
2697
2714
|
else
|
|
2698
2715
|
real_model, real_singular_table_name = model.real_singular(params)
|
|
2699
|
-
|
|
2716
|
+
# If there are any foreign keys that are strings and we've received a blank value, set that to nil
|
|
2717
|
+
internal_params = (ac_params = send(params_name_sym))&.send(:parameters)
|
|
2718
|
+
real_model.reflect_on_all_associations.each do |a|
|
|
2719
|
+
if a.belongs_to? && internal_params.key?(fk_name = a.foreign_key.to_s) && [:string, :text].include?((fk_col = real_model.columns_hash[fk_name])&.type)
|
|
2720
|
+
internal_params[fk_name] = nil if internal_params[fk_name].blank?
|
|
2721
|
+
end
|
|
2722
|
+
end
|
|
2723
|
+
created_obj = model.send(:new, ac_params)
|
|
2700
2724
|
if created_obj.respond_to?(inh_col = model.inheritance_column) && created_obj.send(inh_col) == ''
|
|
2701
2725
|
created_obj.send("#{inh_col}=", model.name)
|
|
2702
2726
|
end
|
|
@@ -2846,14 +2870,14 @@ class Object
|
|
|
2846
2870
|
code << " def #{params_name}\n"
|
|
2847
2871
|
require_txt = model.base_class.name.underscore.tr('/', '_')
|
|
2848
2872
|
is_for_expects = ::ActiveSupport.version >= ::Gem::Version.new('8.0a')
|
|
2849
|
-
|
|
2873
|
+
permits = model._brick_find_permits(model, model._brick_all_fields(true), is_for_expects)
|
|
2850
2874
|
if is_for_expects
|
|
2851
|
-
code << " params.expect(#{require_txt}: #{
|
|
2875
|
+
code << " params.expect(#{require_txt}: #{permits})\n"
|
|
2852
2876
|
self.define_method(params_name) do
|
|
2853
2877
|
params.expect({ model.base_class.name.underscore.tr('/', '_').to_sym => permits })
|
|
2854
2878
|
end
|
|
2855
2879
|
else
|
|
2856
|
-
code << " params.require(:#{require_txt}).permit(#{
|
|
2880
|
+
code << " params.require(:#{require_txt}).permit(#{permits.map(&:inspect).join(', ')})\n"
|
|
2857
2881
|
self.define_method(params_name) do
|
|
2858
2882
|
params.require(model.base_class.name.underscore.tr('/', '_').to_sym).permit(permits)
|
|
2859
2883
|
end
|
data/lib/brick/rails/engine.rb
CHANGED
|
@@ -950,7 +950,10 @@ if (window.brickFontFamily) {
|
|
|
950
950
|
when 'index'
|
|
951
951
|
if Object.const_defined?('DutyFree')
|
|
952
952
|
template_link = "
|
|
953
|
-
<%=
|
|
953
|
+
<%= csv_path = #{@_brick_model._brick_index}_path(format: :csv)
|
|
954
|
+
where_params = params.to_unsafe_h.each_with_object([]) { |v, s| s << \"#\{v.first}=#\{v[1]}\" if v.first.start_with?('__') }.join('&')
|
|
955
|
+
csv_path << \"?#\{where_params}\" unless where_params.blank?
|
|
956
|
+
link_to 'CSV', csv_path %> <a href=\"#\" id=\"sheetsLink\">Sheets</a>
|
|
954
957
|
<div id=\"dropper\" contenteditable=\"true\"></div>
|
|
955
958
|
<input type=\"button\" id=\"btnImport\" value=\"Import\">
|
|
956
959
|
|
|
@@ -1466,21 +1469,11 @@ if (description = rel&.fetch(:description, nil)) %>
|
|
|
1466
1469
|
end
|
|
1467
1470
|
%><%= link_to \"(See all #\{model_name.pluralize})\", see_all_path, { class: '__brick' } %>
|
|
1468
1471
|
#{::Brick::Rails.erd_markup(@_brick_model, prefix) if @_brick_model}
|
|
1469
|
-
<% if obj
|
|
1470
|
-
# path_options = [obj.#{pk}]
|
|
1471
|
-
# path_options << { '_brick_schema': } if
|
|
1472
|
-
options = {}
|
|
1473
|
-
options[:url] = if obj.new_record?
|
|
1474
|
-
link_to_brick(obj.class, path_only: true) # Properly supports STI, but only works for :new
|
|
1475
|
-
else
|
|
1476
|
-
path_helper = obj.new_record? ? #{model_name}._brick_index : #{model_name}._brick_index(:singular)
|
|
1477
|
-
options[:url] = send(\"#\{path_helper}_path\".to_sym, obj) if ::Brick.config.path_prefix || (path_helper != obj.class.table_name)
|
|
1478
|
-
end
|
|
1479
|
-
%>
|
|
1472
|
+
<% if obj %>
|
|
1480
1473
|
<br><br>
|
|
1481
1474
|
|
|
1482
1475
|
<%= # Write out the mega-form
|
|
1483
|
-
|
|
1476
|
+
brick_form_with(model: obj, bts: bts, pk: #{pk.inspect}) %>
|
|
1484
1477
|
|
|
1485
1478
|
#{unless args.first == 'new'
|
|
1486
1479
|
# Was: confirm_are_you_sure = ActionView.version < ::Gem::Version.new('7.0') ? "data: { confirm: \"Delete #\{model_name} -- Are you sure?\" }" : "form: { data: { turbo_confirm: \"Delete #\{model_name} -- Are you sure?\" } }"
|
|
@@ -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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
83
|
+
out << self.text_field(method_sym, html_options)
|
|
83
84
|
end
|
|
84
85
|
when :boolean
|
|
85
|
-
out << self.check_box(
|
|
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(
|
|
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
|
|
93
|
-
out << self.text_field(
|
|
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(
|
|
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 = [
|
|
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(
|
|
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) && [:
|
|
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
|
|
@@ -252,25 +252,52 @@ module Brick::Rails::FormTags
|
|
|
252
252
|
|
|
253
253
|
# -----------------------------
|
|
254
254
|
# Our mega show/new/update form
|
|
255
|
-
def
|
|
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 || []
|
|
256
265
|
pk = [pk] unless pk.is_a?(Array)
|
|
257
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
|
|
258
280
|
form_for(obj.becomes(model.base_class), options) do |f|
|
|
259
|
-
out = +'
|
|
260
|
-
|
|
281
|
+
out = +''
|
|
282
|
+
out << '<table class="shadow">'
|
|
283
|
+
f.instance_variable_set(:@_brick, brick_stuff)
|
|
284
|
+
has_fields = has_updatable_fields = nil
|
|
261
285
|
# If it's a new record, set any default polymorphic types
|
|
262
|
-
bts
|
|
286
|
+
bts.each do |_k, v|
|
|
263
287
|
if v[2]
|
|
264
288
|
obj.send("#{model.brick_foreign_type(v.first)}=", v[1].first&.first&.name)
|
|
265
289
|
end
|
|
266
290
|
end if obj.new_record?
|
|
267
291
|
hoa, hma, rtans = model._activestorage_actiontext_fields
|
|
268
|
-
|
|
292
|
+
|
|
293
|
+
block.call(f) if block_given?
|
|
294
|
+
|
|
295
|
+
((model.column_names + hoa + hma + rtans.keys) - fields_rendered).each do |k|
|
|
269
296
|
pk_pos = (pk.index(k)&.+ 1)
|
|
270
|
-
|
|
297
|
+
col = model.columns_hash[k]
|
|
298
|
+
next if (pk_pos && pk.length == 1 && !bts.key?(k) && [:string, :text].exclude?(col.type)) ||
|
|
271
299
|
::Brick.config.metadata_columns.include?(k)
|
|
272
300
|
|
|
273
|
-
col = model.columns_hash[k]
|
|
274
301
|
if !col
|
|
275
302
|
kwargs = if hoa.include?(k) # has_one_attached
|
|
276
303
|
{ sql_type: 'binary', type: :file }
|
|
@@ -291,7 +318,7 @@ module Brick::Rails::FormTags
|
|
|
291
318
|
val = obj.attributes[k]
|
|
292
319
|
out << "
|
|
293
320
|
<tr>
|
|
294
|
-
<th class=\"show-field\"#{" title=\"#{col&.comment}\""
|
|
321
|
+
<th class=\"show-field\"#{" title=\"#{col&.comment}\"" if col&.respond_to?(:comment) && !col&.comment.blank?}>"
|
|
295
322
|
has_fields = true
|
|
296
323
|
if (bt = bts[k])
|
|
297
324
|
# Add a final member in this array with descriptive options to be used in <select> drop-downs
|
|
@@ -334,35 +361,38 @@ module Brick::Rails::FormTags
|
|
|
334
361
|
else
|
|
335
362
|
out << model.human_attribute_name(k, { default: k })
|
|
336
363
|
end
|
|
337
|
-
out << " (PK
|
|
364
|
+
out << " (PK #{pk_pos})" if pk_pos
|
|
338
365
|
out << "
|
|
339
366
|
</th>
|
|
340
367
|
<td>
|
|
341
368
|
"
|
|
342
|
-
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))
|
|
343
372
|
out << val.to_s
|
|
344
373
|
else
|
|
374
|
+
has_updatable_fields = true
|
|
345
375
|
out << f.brick_field(k, html_options = {}, val, col, bt, bt_class, bt_name, bt_pair)
|
|
346
376
|
end
|
|
347
377
|
out << "
|
|
348
378
|
</td>
|
|
349
379
|
</tr>"
|
|
350
380
|
end
|
|
351
|
-
if
|
|
381
|
+
if has_updatable_fields
|
|
352
382
|
out << "<tr><td colspan=\"2\">#{f.submit({ class: 'update' })}</td></tr>"
|
|
353
|
-
|
|
383
|
+
elsif !has_fields
|
|
354
384
|
out << '<tr><td colspan="2">(No displayable fields)</td></tr>'
|
|
355
385
|
end
|
|
356
386
|
out << '</table>'
|
|
357
387
|
if model.name == 'ActiveStorage::Attachment'
|
|
358
388
|
begin
|
|
359
|
-
out << ::Brick::Rails.display_binary(obj&.blob&.download, 500_000)
|
|
389
|
+
out << ::Brick::Rails.display_binary(obj&.blob&.download, 500_000)
|
|
360
390
|
rescue
|
|
361
391
|
end
|
|
362
392
|
end
|
|
363
|
-
out.html_safe
|
|
364
|
-
end
|
|
365
|
-
end #
|
|
393
|
+
output_buffer << out.html_safe
|
|
394
|
+
end # form_for
|
|
395
|
+
end # brick_form_with
|
|
366
396
|
|
|
367
397
|
# ------------------------------------------
|
|
368
398
|
# Our cool N:M checkbox constellation editor
|
|
@@ -413,7 +443,7 @@ module Brick::Rails::FormTags
|
|
|
413
443
|
end
|
|
414
444
|
out << " </tbody>
|
|
415
445
|
</table>
|
|
416
|
-
<script
|
|
446
|
+
<script#{@_request.env['_brick_nonce']}>
|
|
417
447
|
var constellation = document.getElementById(\"#{table_name}\");
|
|
418
448
|
var nextSib,
|
|
419
449
|
_this;
|
|
@@ -480,7 +510,6 @@ module Brick::Rails::FormTags
|
|
|
480
510
|
out << " </tbody>
|
|
481
511
|
</table>
|
|
482
512
|
"
|
|
483
|
-
|
|
484
513
|
out.html_safe
|
|
485
514
|
end # brick_bezier
|
|
486
515
|
|
|
@@ -537,7 +566,7 @@ module Brick::Rails::FormTags
|
|
|
537
566
|
</div>
|
|
538
567
|
</div>
|
|
539
568
|
"
|
|
540
|
-
out
|
|
569
|
+
out.html_safe
|
|
541
570
|
end # brick_header
|
|
542
571
|
|
|
543
572
|
# All the standard CSS with teal colouration for use with Brick
|
|
@@ -547,6 +576,8 @@ module Brick::Rails::FormTags
|
|
|
547
576
|
|
|
548
577
|
# -----------------------------------------------------------------------------------------------
|
|
549
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!
|
|
550
581
|
table_name = klass.table_name.split('.').last
|
|
551
582
|
|
|
552
583
|
# Javascript for brick_grid and brick_constellation
|
|
@@ -556,13 +587,13 @@ module Brick::Rails::FormTags
|
|
|
556
587
|
// Plunk the row count in now that we know it
|
|
557
588
|
var rowCount = document.getElementById(\"rowCount\");
|
|
558
589
|
if (rowCount) rowCount.innerHTML = \"#{pluralize(row_count, "row")}#{total_row_count} \";
|
|
559
|
-
var #{
|
|
590
|
+
var #{camel_name}HtColumns;
|
|
560
591
|
" unless row_count.nil?
|
|
561
592
|
|
|
562
593
|
grid_scripts << "
|
|
563
594
|
// Snag first TR for sticky header
|
|
564
595
|
var grid = document.getElementById(\"#{table_name}\");
|
|
565
|
-
#{
|
|
596
|
+
#{camel_name}HtColumns = grid && [grid.getElementsByTagName(\"TR\")[0]];
|
|
566
597
|
var headerTop = document.getElementById(\"headerTop\");
|
|
567
598
|
var headerCols;
|
|
568
599
|
if (grid) {
|
|
@@ -615,8 +646,8 @@ function setHeaderSizes() {
|
|
|
615
646
|
|
|
616
647
|
// Set up proper sizings of sticky column header
|
|
617
648
|
var node;
|
|
618
|
-
for (var j = 0; j < #{
|
|
619
|
-
var row = #{
|
|
649
|
+
for (var j = 0; j < #{camel_name}HtColumns.length; ++j) {
|
|
650
|
+
var row = #{camel_name}HtColumns[j];
|
|
620
651
|
var tr = isEmpty ? document.createElement(\"TR\") : headerTop.childNodes[j];
|
|
621
652
|
tr.innerHTML = row.innerHTML.trim();
|
|
622
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
|
|
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
|
data/lib/brick/reflect_tables.rb
CHANGED
|
@@ -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
|
|
|
@@ -443,14 +445,18 @@ ORDER BY 1, 2, c.internal_column_id, acc.position"
|
|
|
443
445
|
loop do
|
|
444
446
|
klass = Object
|
|
445
447
|
proposed_name_parts.each do |part|
|
|
446
|
-
|
|
447
|
-
|
|
448
|
+
begin
|
|
449
|
+
if klass.const_defined?(part) && klass.name != part
|
|
448
450
|
klass = klass.const_get(part)
|
|
449
|
-
|
|
451
|
+
else
|
|
450
452
|
klass = nil
|
|
451
453
|
break
|
|
452
454
|
end
|
|
453
|
-
|
|
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."
|
|
454
460
|
klass = nil
|
|
455
461
|
break
|
|
456
462
|
end
|
data/lib/brick/version_number.rb
CHANGED
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 =
|
|
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
|
-
|
|
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]
|
|
@@ -119,7 +119,8 @@ module Brick
|
|
|
119
119
|
stuck = {}
|
|
120
120
|
indexes = {} # Track index names to make sure things are unique
|
|
121
121
|
built_schemas = {} # Track all built schemas so we can place an appropriate drop_schema command only in the first
|
|
122
|
-
|
|
122
|
+
# migration in which that schema is referenced, thereby allowing rollbacks to function properly.
|
|
123
|
+
is_airtable = nil
|
|
123
124
|
versions_to_create = [] # Resulting versions to be used when updating the schema_migrations table
|
|
124
125
|
# Start by making migrations for fringe tables (those with no foreign keys).
|
|
125
126
|
# Continue layer by layer, creating migrations for tables that reference ones already done, until
|
|
@@ -148,8 +149,9 @@ module Brick
|
|
|
148
149
|
mig = gen_migration_columns(relations, tbl, (tbl_parts = tbl.split('.')), (add_fks = []), built_schemas, mig_path, current_mig_time,
|
|
149
150
|
key_type, is_4x_rails, ar_version, do_fks_last, versions_to_create)
|
|
150
151
|
after_fks.concat(add_fks) if do_fks_last
|
|
151
|
-
current_mig_time
|
|
152
|
-
|
|
152
|
+
increment_time(mig_path, current_mig_time)
|
|
153
|
+
is_airtable ||= (relation = relations[tbl])&.fetch(:airtable_table, nil)
|
|
154
|
+
versions_to_create << migration_file_write(mig_path, "create_#{::Brick._brick_index(tbl, nil, separator, relation, true)}", current_mig_time, ar_version, mig)
|
|
153
155
|
end
|
|
154
156
|
done.concat(fringe)
|
|
155
157
|
chosen -= done
|
|
@@ -161,15 +163,15 @@ module Brick
|
|
|
161
163
|
mig = gen_migration_columns(relations, tbl, (tbl_parts = tbl.split('.')), (add_fks = []), built_schemas, mig_path, current_mig_time,
|
|
162
164
|
key_type, is_4x_rails, ar_version, do_fks_last, versions_to_create)
|
|
163
165
|
after_fks.concat(add_fks)
|
|
164
|
-
current_mig_time
|
|
166
|
+
increment_time(mig_path, current_mig_time)
|
|
165
167
|
versions_to_create << migration_file_write(mig_path, "create_#{
|
|
166
|
-
::Brick._brick_index(tbl, :migration, separator, relations[tbl])
|
|
168
|
+
::Brick._brick_index(tbl, :migration, separator, relations[tbl], true)
|
|
167
169
|
}", current_mig_time, ar_version, mig)
|
|
168
170
|
end
|
|
169
171
|
done.concat(chosen)
|
|
170
172
|
chosen.clear
|
|
171
173
|
|
|
172
|
-
case do_fks_last
|
|
174
|
+
case after_fks.present? && do_fks_last
|
|
173
175
|
when 'Separate' # Add a final migration to create all the foreign keys
|
|
174
176
|
mig = +" def change\n"
|
|
175
177
|
after_fks.each do |add_fk|
|
|
@@ -201,9 +203,10 @@ module Brick
|
|
|
201
203
|
'rb'
|
|
202
204
|
end
|
|
203
205
|
mig << +" end\n"
|
|
204
|
-
current_mig_time
|
|
205
|
-
|
|
206
|
-
|
|
206
|
+
increment_time(mig_path, current_mig_time)
|
|
207
|
+
base_name = "_#{relations[:base_name]&.tr(' ', '')&.underscore}" if relations[:base_name]
|
|
208
|
+
versions_to_create << migration_file_write(mig_path, "create_brick_fks#{base_name}.#{fks_extension}", current_mig_time, ar_version, mig)
|
|
209
|
+
puts "Have written out a final migration called 'create_brick_fks#{base_name}.#{fks_extension}' which creates #{after_fks.length} foreign keys."
|
|
207
210
|
if fks_extension == 'rbx'
|
|
208
211
|
puts " This file extension (.rbx) will cause it not to run yet when you do a 'rails db:migrate'.
|
|
209
212
|
The idea here is to do all data loading first, and then rename that migration file back
|
|
@@ -247,7 +250,7 @@ module Brick
|
|
|
247
250
|
". Here's the top 5 blockers" if stuck_sorted.length > 5
|
|
248
251
|
}:"
|
|
249
252
|
pp stuck_sorted[0..4]
|
|
250
|
-
elsif do_schema_migrations # Successful, and now we can update the schema_migrations table accordingly
|
|
253
|
+
elsif do_schema_migrations && !is_airtable # Successful, and now we can update the schema_migrations table accordingly
|
|
251
254
|
smtn = ::Brick._schema_migrations_table_name
|
|
252
255
|
if ActiveRecord::Migration.table_exists?(smtn)
|
|
253
256
|
# Remove to_delete - to_create
|
|
@@ -298,7 +301,7 @@ module Brick
|
|
|
298
301
|
end
|
|
299
302
|
unless schema.blank? || built_schemas.key?(schema)
|
|
300
303
|
mig = +" def change\n create_schema(:#{schema}) unless schema_exists?(:#{schema})\n end\n"
|
|
301
|
-
current_mig_time
|
|
304
|
+
increment_time(mig_path, current_mig_time)
|
|
302
305
|
versions_to_create << migration_file_write(mig_path, "create_db_schema_#{schema.underscore}", current_mig_time, ar_version, mig)
|
|
303
306
|
built_schemas[schema] = nil
|
|
304
307
|
end
|
|
@@ -352,7 +355,8 @@ module Brick
|
|
|
352
355
|
mig = +" def change\n return unless reverting? || !table_exists?(#{tbl_code})\n\n"
|
|
353
356
|
mig << " create_table #{tbl_code}#{id_option} do |t|\n"
|
|
354
357
|
possible_ts = [] # Track possible generic timestamps
|
|
355
|
-
relation[:
|
|
358
|
+
(relation.key?(:col_order) && relation[:col_order].present? && relation[:col_order].map { |col_name| [col_name, relation[:cols][col_name]] } ||
|
|
359
|
+
relation[:cols]).each do |col, col_type|
|
|
356
360
|
sql_type = SQL_TYPES[col_type.first] || SQL_TYPES[col_type[0..1]] ||
|
|
357
361
|
SQL_TYPES.find { |r| r.first.is_a?(Regexp) && col_type.first =~ r.first }&.last ||
|
|
358
362
|
col_type.first
|
|
@@ -449,7 +453,16 @@ module Brick
|
|
|
449
453
|
" t.#{type.start_with?('numeric') ? 'decimal' : type} :#{name}#{suffix}\n"
|
|
450
454
|
end
|
|
451
455
|
|
|
456
|
+
def increment_time(mig_path, current_mig_time)
|
|
457
|
+
loop do
|
|
458
|
+
current_mig_time[0] += 1.minute
|
|
459
|
+
break if Dir.glob("#{mig_path}/#{current_mig_time.first.strftime('%Y%m%d%H%M00')}_*.*").empty?
|
|
460
|
+
|
|
461
|
+
end
|
|
462
|
+
end
|
|
463
|
+
|
|
452
464
|
def migration_file_write(mig_path, name, current_mig_time, ar_version, mig)
|
|
465
|
+
name.gsub!(/[^a-zA-Z0-9_\.\p{L}\p{N}]/, 'x') # Strip out any weird unicode characters
|
|
453
466
|
File.open("#{mig_path}/#{version = current_mig_time.first.strftime('%Y%m%d%H%M00')}_#{name}#{'.rb' unless name.index('.')}", "w") do |f|
|
|
454
467
|
f.write "class #{name.split('.').first.camelize} < ActiveRecord::Migration#{ar_version}\n"
|
|
455
468
|
f.write mig
|
|
@@ -13,9 +13,10 @@ module Brick
|
|
|
13
13
|
end
|
|
14
14
|
|
|
15
15
|
def generate_seeds(relations = nil)
|
|
16
|
+
action = 'Overwrite'
|
|
16
17
|
if File.exist?((seed_file_path = "#{::Rails.root}/db/seed") + 's.rb')
|
|
17
|
-
puts "WARNING: seeds file #{seed_file_path}s.rb appears to already be present.\
|
|
18
|
-
return
|
|
18
|
+
puts "WARNING: seeds file #{seed_file_path}s.rb appears to already be present.\nAppend or Overwrite?"
|
|
19
|
+
return if (action = gets_list(list: ['Cancel', 'Append', 'Overwrite'])) == 'Cancel'
|
|
19
20
|
|
|
20
21
|
puts "\n"
|
|
21
22
|
end
|
|
@@ -24,7 +25,7 @@ module Brick
|
|
|
24
25
|
is_airtable = true # So far the only thing that feeds us relations is Airtable
|
|
25
26
|
require 'generators/brick/airtable_api_caller'
|
|
26
27
|
# include ::Brick::MigrationsBuilder
|
|
27
|
-
chosen = relations.
|
|
28
|
+
chosen = relations.each_with_object([]) { |v, s| s << SeedModel.new(v.first, nil, false, v.last[:airtable_table]) unless v.first.is_a?(Symbol) }
|
|
28
29
|
else
|
|
29
30
|
::Brick.mode = :on
|
|
30
31
|
ActiveRecord::Base.establish_connection
|
|
@@ -57,11 +58,14 @@ module Brick
|
|
|
57
58
|
end
|
|
58
59
|
end
|
|
59
60
|
batch_num = 1
|
|
60
|
-
seeds_file = File.open("#{seed_file_path}s.rb", 'w')
|
|
61
|
+
seeds_file = File.open("#{seed_file_path}s.rb", action == 'Overwrite' ? 'w' : 'a+')
|
|
62
|
+
seeds_file.write("\n") if action == 'Append'
|
|
61
63
|
seeds_len = lines_len = 0
|
|
62
64
|
seeds_file.write('# Seeds file for ')
|
|
63
65
|
seed_rows = nil
|
|
64
|
-
if
|
|
66
|
+
if is_airtable
|
|
67
|
+
seeds_file.write("Airtable Base \"#{relations[:base_name]}\":\n")
|
|
68
|
+
elsif (arbc = ActiveRecord::Base.connection).respond_to?(:current_database) # SQLite3 can't do this!
|
|
65
69
|
seeds_file.write("#{arbc.current_database}:\n")
|
|
66
70
|
elsif (filename = arbc.instance_variable_get(:@connection_parameters)&.fetch(:database, nil))
|
|
67
71
|
seeds_file.write("#{filename}:\n")
|
|
@@ -146,19 +150,38 @@ module Brick
|
|
|
146
150
|
|
|
147
151
|
has_rows = false
|
|
148
152
|
is_empty = true
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
153
|
+
collection = nil
|
|
154
|
+
klass_name = if is_airtable
|
|
155
|
+
airtable_table = relation[:airtable_table]
|
|
156
|
+
::Brick.config.table_name_prefixes&.fetch(tbl, nil) ||
|
|
157
|
+
tbl.singularize.camelize
|
|
158
|
+
else
|
|
159
|
+
# Must convert to a symbol when doing ORDER BY due to this ActiveRecord nuance:
|
|
160
|
+
# https://github.com/rails/rails/issues/2601
|
|
161
|
+
collection = klass.order(*pkey_cols.map(&:to_sym)).enum_for unless relation[:pkey].empty?
|
|
162
|
+
klass.name
|
|
163
|
+
end
|
|
160
164
|
seed_rows = +''
|
|
161
|
-
|
|
165
|
+
airtable_idx = offset = nil
|
|
166
|
+
while collection || airtable_table do
|
|
167
|
+
obj = if is_airtable
|
|
168
|
+
if collection.nil? || (airtable_idx += 1) >= collection.length
|
|
169
|
+
break if collection && !offset
|
|
170
|
+
|
|
171
|
+
offset_param = "?offset=#{offset}" if offset
|
|
172
|
+
result = ::Brick::AirtableApiCaller.https_get("https://api.airtable.com/v0/#{airtable_table.base_id}/#{airtable_table.id}#{offset_param}")
|
|
173
|
+
offset = result.fetch('offset', nil)
|
|
174
|
+
collection = result.fetch('records', nil)
|
|
175
|
+
airtable_idx = 0
|
|
176
|
+
end
|
|
177
|
+
collection[airtable_idx]
|
|
178
|
+
else
|
|
179
|
+
begin
|
|
180
|
+
collection.next
|
|
181
|
+
rescue StopIteration
|
|
182
|
+
break
|
|
183
|
+
end
|
|
184
|
+
end
|
|
162
185
|
if is_airtable
|
|
163
186
|
fields = obj['fields'].each_with_object({}) do |field, s|
|
|
164
187
|
if relation[:cols].keys.include?(col_name = ::Brick::AirtableApiCaller.sane_name(field.first))
|
|
@@ -178,7 +201,7 @@ module Brick
|
|
|
178
201
|
end
|
|
179
202
|
end
|
|
180
203
|
end
|
|
181
|
-
objects =
|
|
204
|
+
objects = airtable_table.objects
|
|
182
205
|
obj = objects[airtable_id = obj['id']] = AirtableObject.new(seed_model, obj['fields'], obj['createdTime'])
|
|
183
206
|
end
|
|
184
207
|
unless has_rows
|
|
@@ -204,7 +227,12 @@ module Brick
|
|
|
204
227
|
# Used to be: obj.send(col)
|
|
205
228
|
# (and with that it was possible to raise ActiveRecord::Encryption::Errors::Configuration...)
|
|
206
229
|
# %%% should test further and see if that is possible with this code!)
|
|
207
|
-
|
|
230
|
+
val = if is_airtable && col == 'airtable_id'
|
|
231
|
+
pk_val
|
|
232
|
+
else
|
|
233
|
+
obj.attributes_before_type_cast[col]
|
|
234
|
+
end
|
|
235
|
+
if val
|
|
208
236
|
if (val.is_a?(Time) || val.is_a?(Date))
|
|
209
237
|
val = val.to_s
|
|
210
238
|
elsif val.is_a?(Numeric)
|
|
@@ -283,28 +311,42 @@ end\n"
|
|
|
283
311
|
end
|
|
284
312
|
seed_rows = +''
|
|
285
313
|
end
|
|
286
|
-
unless has_rows ||
|
|
314
|
+
unless has_rows || (is_airtable && !airtable_table)
|
|
287
315
|
seeds_file.write(" # (Skipping #{klass_name} as it has no rows)\n")
|
|
288
316
|
end
|
|
289
317
|
end
|
|
290
318
|
done.concat(fringe)
|
|
291
319
|
chosen -= done
|
|
292
320
|
end
|
|
293
|
-
|
|
294
|
-
airtable_assoc_recids.
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
321
|
+
|
|
322
|
+
if airtable_assoc_recids.present?
|
|
323
|
+
seed_rows << "\n# Seeding #{airtable_assoc_recids.count} sets of Airtable associations:\n"
|
|
324
|
+
airtable_assocs_done = {}
|
|
325
|
+
dupe_list = []
|
|
326
|
+
airtable_assoc_recids.each do |table_name, assoc_pairs| # N:M links
|
|
327
|
+
# Make note of any other tables which have exactly duplicated data
|
|
328
|
+
dupes = airtable_assocs_done.select do |dupe_table, dupe_pairs|
|
|
329
|
+
dupe_table != table_name && assoc_pairs.length > 0 && assoc_pairs.length == dupe_pairs.length &&
|
|
330
|
+
dupe_pairs.all? { |pair_name, _v3| assoc_pairs.keys.include?(pair_name) }
|
|
331
|
+
end
|
|
332
|
+
seed_rows << " if ActiveRecord::Migration.table_exists?('#{table_name}')\n"
|
|
333
|
+
unless dupes.empty?
|
|
334
|
+
seed_rows << " # Duplicate data found in: #{dupes.keys.join(', ')}\n"
|
|
335
|
+
dupes.keys.each { |k| dupe_list << "#{table_name} | #{k}" }
|
|
336
|
+
end
|
|
337
|
+
seed_rows << " puts 'Airtable associations for #{table_name.camelize.singularize}'\n"
|
|
338
|
+
assoc_pairs.each do |_k2, pair_values|
|
|
339
|
+
seed_rows << "#{table_name.singularize.camelize}.create(#{pair_values})\n"
|
|
340
|
+
end
|
|
341
|
+
seed_rows << " end\n"
|
|
342
|
+
airtable_assocs_done[table_name] = assoc_pairs
|
|
299
343
|
end
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
344
|
+
|
|
345
|
+
if dupe_list.present?
|
|
346
|
+
puts "\nAssociative tables with duplicate data:"
|
|
347
|
+
dupe_list.each { |entry| puts " #{entry}" }
|
|
348
|
+
puts '(You can safely remove one of the two tables in each of the pairs listed above.)'
|
|
305
349
|
end
|
|
306
|
-
seed_rows << " end\n"
|
|
307
|
-
airtable_assocs_done[table_name] = assoc_pairs
|
|
308
350
|
end
|
|
309
351
|
seeds_file.write(seed_rows) unless seed_rows.blank?
|
|
310
352
|
|