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.
- checksums.yaml +4 -4
- data/lib/brick/extensions.rb +123 -55
- data/lib/brick/rails/engine.rb +41 -19
- data/lib/brick/reflect_tables.rb +54 -22
- data/lib/brick/version_number.rb +1 -1
- data/lib/brick.rb +5 -1
- data/lib/generators/brick/airtable_api_caller.rb +171 -0
- data/lib/generators/brick/airtable_migrations_generator.rb +24 -0
- data/lib/generators/brick/airtable_seeds_generator.rb +19 -0
- data/lib/generators/brick/install_generator.rb +18 -6
- data/lib/generators/brick/{migration_builder.rb → migrations_builder.rb} +7 -3
- data/lib/generators/brick/migrations_generator.rb +4 -4
- data/lib/generators/brick/salesforce_migrations_generator.rb +3 -3
- data/lib/generators/brick/salesforce_schema.rb +1 -1
- data/lib/generators/brick/seeds_builder.rb +329 -0
- data/lib/generators/brick/seeds_generator.rb +2 -242
- metadata +7 -3
@@ -0,0 +1,171 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Brick
|
4
|
+
class AirtableApiCaller
|
5
|
+
class << self
|
6
|
+
include FancyGets
|
7
|
+
|
8
|
+
def pick_tables(usage = :migrations)
|
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
|
+
https://airtable.com/create/tokens
|
11
|
+
You need only #{usage == :migrations ? 'this scope:' : "these three scopes:
|
12
|
+
data.records:read
|
13
|
+
data.recordComments:read"}
|
14
|
+
schema.bases:read
|
15
|
+
|
16
|
+
Please provide your Airtable PAT:"
|
17
|
+
pat = gets_password
|
18
|
+
require 'net/http'
|
19
|
+
# Generate a list of bases that can be chosen
|
20
|
+
bases = https_get('https://api.airtable.com/v0/meta/bases', pat)
|
21
|
+
base = gets_list(bases.fetch('bases', nil)&.map { |z| AirtableTable.new(z['id'], z['name']) })
|
22
|
+
puts
|
23
|
+
# Generate a list of tables that can be chosen
|
24
|
+
objects = https_get("https://api.airtable.com/v0/meta/bases/#{base.id}/tables", pat).fetch('tables', nil)
|
25
|
+
if objects.blank?
|
26
|
+
puts "No tables found in base #{base.name}."
|
27
|
+
return
|
28
|
+
end
|
29
|
+
|
30
|
+
tables = objects.map { |z| AirtableTable.new(z['id'], z['name'], z['primaryFieldId'], z['fields'], z['views'], base.id) }
|
31
|
+
chosen = gets_list(tables, tables.dup)
|
32
|
+
puts
|
33
|
+
|
34
|
+
# Build out a '::Brick.relations' hash that represents this Airtable schema
|
35
|
+
fks = []
|
36
|
+
associatives = {}
|
37
|
+
relations = chosen.each_with_object({}) do |table, s|
|
38
|
+
tbl_name = sane_table_name(table.name)
|
39
|
+
# Build out columns and foreign keys
|
40
|
+
cols = {}
|
41
|
+
table.fields.each do |col|
|
42
|
+
col_name = sane_name(col['name'])
|
43
|
+
# This is like a has_many or has_many through
|
44
|
+
if col['type'] == 'multipleRecordLinks'
|
45
|
+
# binding.pry if col['options']['isReversed']
|
46
|
+
if (frn_tbl = sane_table_name(
|
47
|
+
chosen.find { |t| t.id == col['options']['linkedTableId'] }&.name
|
48
|
+
))
|
49
|
+
if col['options']['prefersSingleRecordLink'] # 1:M
|
50
|
+
fks << [frn_tbl, "#{col_name}_id", tbl_name, col_name]
|
51
|
+
else # N:M
|
52
|
+
# Queue up to build associative table with two foreign keys
|
53
|
+
camelized = (assoc_name = "#{tbl_name}_#{col_name}_#{frn_tbl}").camelize
|
54
|
+
if associatives.keys.any? { |a| a.camelize == camelized }
|
55
|
+
puts "Strangely have found two columns in \"#{table.name}\" with a name similar to \"#{col_name}\". Skipping this to avoid a conflict."
|
56
|
+
next
|
57
|
+
|
58
|
+
end
|
59
|
+
associatives[assoc_name] = [col_name, frn_tbl, tbl_name]
|
60
|
+
fks << [assoc_name, frn_tbl, frn_tbl, col_name.underscore, tbl_name]
|
61
|
+
end
|
62
|
+
end
|
63
|
+
else
|
64
|
+
# puts col['type']
|
65
|
+
dt = case col['type']
|
66
|
+
when 'singleLineText', 'url', 'singleSelect'
|
67
|
+
'string'
|
68
|
+
when 'multilineText'
|
69
|
+
'text'
|
70
|
+
when 'number'
|
71
|
+
'decimal'
|
72
|
+
when 'checkbox'
|
73
|
+
'boolean'
|
74
|
+
when 'date'
|
75
|
+
'date'
|
76
|
+
when 'multipleSelects'
|
77
|
+
# Sqlite3 can do json
|
78
|
+
'json'
|
79
|
+
when 'formula', 'count', 'rollup', 'multipleAttachments'
|
80
|
+
next
|
81
|
+
# else
|
82
|
+
# binding.pry
|
83
|
+
end
|
84
|
+
cols[col_name] = [dt, nil, true, false] # true is the col[:nillable]
|
85
|
+
end
|
86
|
+
end
|
87
|
+
# Put it all into a relation entry, named the same as the table
|
88
|
+
pkey = table.fields.find { |f| f['id'] == table.primary_key }['name']
|
89
|
+
s[tbl_name] = {
|
90
|
+
pkey: { "#{tbl_name}_pkey" => [sane_name(pkey)] },
|
91
|
+
cols: cols,
|
92
|
+
fks: {},
|
93
|
+
airtable_table: table
|
94
|
+
}
|
95
|
+
end
|
96
|
+
associatives.each do |k, v|
|
97
|
+
pri_pk_col = relations[v[1]][:pkey]&.first&.last&.first
|
98
|
+
frn_pk_col = relations[v[2]][:pkey]&.first&.last&.first
|
99
|
+
pri_fk_name = "#{v[1]}_id"
|
100
|
+
frn_fk_name = (frn_fk_name == pri_fk_name) ?
|
101
|
+
"#{v[2]}_2_id" # Self-referencing N:M
|
102
|
+
: "#{v[2]}_id" # Standard N:M
|
103
|
+
relations[k] = {
|
104
|
+
pkey: { "#{k}_pkey" => ['id'] },
|
105
|
+
cols: { 'id' => ['integer', nil, false, false] }
|
106
|
+
}
|
107
|
+
fks << [v[1], pri_fk_name, k, pri_fk_name.underscore]
|
108
|
+
fks << [v[2], frn_fk_name, k, frn_fk_name.underscore]
|
109
|
+
end
|
110
|
+
fk_idx = 0
|
111
|
+
fks.each do |pri_tbl, fk_col, frn_tbl, airtable_col, assoc_tbl|
|
112
|
+
pri_pk_col = relations[pri_tbl][:pkey].first.last.first
|
113
|
+
# binding.pry unless relations.key?(frn_tbl) && relations[pri_tbl][:cols][pri_pk_col]
|
114
|
+
unless assoc_tbl # It's a 1:M -- make a FK column
|
115
|
+
relations[frn_tbl][:cols][fk_col] = [relations[pri_tbl][:cols][pri_pk_col][0], nil, true, false]
|
116
|
+
end
|
117
|
+
# And the actual relation
|
118
|
+
frn_fks = ((relations[frn_tbl] ||= {})[:fks] ||= {})
|
119
|
+
this_fk = frn_fks["fk_airtable_#{fk_idx += 1}"] = {
|
120
|
+
is_bt: !assoc_tbl, # Normal foreign key is true, and N:M is really a has_many, so false
|
121
|
+
fk: fk_col,
|
122
|
+
assoc_name: airtable_col,
|
123
|
+
inverse_table: pri_tbl
|
124
|
+
}
|
125
|
+
this_fk[:assoc_tbl] = assoc_tbl if assoc_tbl
|
126
|
+
end
|
127
|
+
|
128
|
+
relations
|
129
|
+
end
|
130
|
+
|
131
|
+
def https_get(uri, pat = nil)
|
132
|
+
uri = URI(uri) unless uri.is_a?(URI)
|
133
|
+
https = Net::HTTP.new(uri.host, uri.port)
|
134
|
+
request = Net::HTTP::Get.new("#{uri.path}?#{uri.query}")
|
135
|
+
request['Authorization'] = "Bearer #{@bearer ||= pat}"
|
136
|
+
response = Net::HTTP.start(uri.hostname, uri.port, { use_ssl: true }) do |http|
|
137
|
+
http.request(request)
|
138
|
+
end
|
139
|
+
# if response.code&.to_i > 299
|
140
|
+
# end
|
141
|
+
JSON.parse(response.body)
|
142
|
+
end
|
143
|
+
|
144
|
+
def sane_name(col_name)
|
145
|
+
sane_table_name(col_name.gsub('&', 'and').tr('()?', ''))
|
146
|
+
end
|
147
|
+
|
148
|
+
def sane_table_name(tbl_name)
|
149
|
+
tbl_name&.downcase&.tr(': -', '_')
|
150
|
+
end
|
151
|
+
|
152
|
+
class AirtableTable
|
153
|
+
attr_accessor :id, :name, :primary_key, :fields, :views, :base_id, :objects
|
154
|
+
def initialize(id, name,
|
155
|
+
primary_key = nil, fields = nil, views = nil, base_id = nil)
|
156
|
+
self.id = id
|
157
|
+
self.name = name
|
158
|
+
self.primary_key = primary_key
|
159
|
+
self.fields = fields
|
160
|
+
self.views = views
|
161
|
+
self.base_id = base_id
|
162
|
+
self.objects = {}
|
163
|
+
end
|
164
|
+
|
165
|
+
def to_s
|
166
|
+
name
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'brick'
|
4
|
+
require 'rails/generators'
|
5
|
+
require 'rails/generators/active_record'
|
6
|
+
require 'fancy_gets'
|
7
|
+
require 'generators/brick/migrations_builder'
|
8
|
+
require 'generators/brick/airtable_api_caller'
|
9
|
+
|
10
|
+
module Brick
|
11
|
+
# Auto-generates Airtable migration files
|
12
|
+
class AirtableMigrationsGenerator < ::Rails::Generators::Base
|
13
|
+
desc 'Auto-generates migration files for an existing Airtable "base".'
|
14
|
+
|
15
|
+
def airtable_migrations
|
16
|
+
mig_path, is_insert_versions, is_delete_versions = ::Brick::MigrationsBuilder.check_folder
|
17
|
+
return unless mig_path &&
|
18
|
+
(relations = ::Brick::AirtableApiCaller.pick_tables)
|
19
|
+
|
20
|
+
::Brick::MigrationsBuilder.generate_migrations(relations.keys, mig_path, is_insert_versions, is_delete_versions, relations,
|
21
|
+
do_fks_last: 'Separate', do_schema_migrations: false)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'brick'
|
4
|
+
require 'rails/generators'
|
5
|
+
require 'rails/generators/active_record'
|
6
|
+
require 'generators/brick/seeds_builder'
|
7
|
+
require 'generators/brick/airtable_api_caller'
|
8
|
+
|
9
|
+
module Brick
|
10
|
+
class AirtableSeedsGenerator < ::Rails::Generators::Base
|
11
|
+
desc 'Auto-generates a seeds file from existing data in an Airtable "base".'
|
12
|
+
|
13
|
+
def airtable_seeds
|
14
|
+
return unless (relations = ::Brick::AirtableApiCaller.pick_tables(:seeds))
|
15
|
+
|
16
|
+
::Brick::SeedsBuilder.generate_seeds(relations)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -346,7 +346,18 @@ if ActiveRecord::Base.respond_to?(:brick_select) && !::Brick.initializer_loaded
|
|
346
346
|
# Brick.model_descrips = { 'User' => '[profile.firstname] [profile.lastname]' }
|
347
347
|
|
348
348
|
# # FULL TEXT SEARCH
|
349
|
+
# # You can enable Elasticsearch support by adding the elasticsearch-model and elasticsearch-rails gems, and either
|
350
|
+
# # having a copy of Opensearch or Elasticsearch locally installed on the same machine listening on port 9200, or by
|
351
|
+
# # setting the ELASTICSEARCH_URL environment variable to point to the URI of a search machine.
|
352
|
+
# # With that configured, you can pick specific table names and permissions for search and update by putting them in
|
353
|
+
# # a hash like this:
|
354
|
+
# Brick.elasticsearch_models = { 'notes' => 'crud', 'issues' => 'cru', 'orders' => 'r' }
|
355
|
+
# # or to blanketly enable all models to have auto-updating CRUD behaviour when there are ActiveRecord changes, use:
|
349
356
|
# Brick.elasticsearch_models = :all
|
357
|
+
# # As well there is another permission available -- the 'i' permission -- which will auto-create an index if it
|
358
|
+
# # is missing. If you set 'icrud' for a model it will auto-create an index, or to always do this for all models
|
359
|
+
# # then you can specify \"full control\" like this:
|
360
|
+
# Brick.elasticsearch_models = :full
|
350
361
|
|
351
362
|
# # ERD SETTINGS
|
352
363
|
|
@@ -377,18 +388,19 @@ if ActiveRecord::Base.respond_to?(:brick_select) && !::Brick.initializer_loaded
|
|
377
388
|
|
378
389
|
# # Polymorphic associations are set up by providing a model name and polymorphic association name#{poly}
|
379
390
|
|
380
|
-
# #
|
381
|
-
|
382
|
-
# #
|
383
|
-
# #
|
391
|
+
# # MULTITENANCY VIA THE ROS-APARTMENT GEM
|
392
|
+
|
393
|
+
# # If you are using the ros-apartment gem along with Postgres then you can have automatic detection of polymorphic
|
394
|
+
# # type names (model class names listed in a column such as imageable_type) by choosing a schema that is loaded up
|
395
|
+
# # with data that represents the full range of the various polymorphic has_many classes that should be associated.
|
384
396
|
# Brick.schema_behavior = :namespaced
|
385
397
|
#{Brick.config.schema_behavior.present? ? " Brick.schema_behavior = { multitenant: { schema_to_analyse: #{
|
386
398
|
Brick.config.schema_behavior[:multitenant]&.fetch(:schema_to_analyse, nil).inspect}" :
|
387
399
|
" # Brick.schema_behavior = { multitenant: { schema_to_analyse: 'engineering'"
|
388
400
|
} } }
|
389
401
|
#{"
|
390
|
-
# # Note that
|
391
|
-
# #
|
402
|
+
# # Note that for each polymorphic model configured then it is better to set the list of classes up in the model
|
403
|
+
# # file itself with a line like:
|
392
404
|
# delegated_type :commentable, type: ['Post', 'Comment']" if ActiveRecord::Base.respond_to?(:delegated_type)}
|
393
405
|
|
394
406
|
# # DEFAULT ROOT ROUTE
|
@@ -1,5 +1,7 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Brick
|
2
|
-
module
|
4
|
+
module MigrationsBuilder
|
3
5
|
# Many SQL types are the same as their migration data type name:
|
4
6
|
# text, integer, bigint, date, boolean, decimal, float
|
5
7
|
# These however are not:
|
@@ -147,7 +149,7 @@ module Brick
|
|
147
149
|
key_type, is_4x_rails, ar_version, do_fks_last, versions_to_create)
|
148
150
|
after_fks.concat(add_fks) if do_fks_last
|
149
151
|
current_mig_time[0] += 1.minute
|
150
|
-
versions_to_create << migration_file_write(mig_path, "create_#{::Brick._brick_index(tbl, nil, separator)}", current_mig_time, ar_version, mig)
|
152
|
+
versions_to_create << migration_file_write(mig_path, "create_#{::Brick._brick_index(tbl, nil, separator, relations[tbl])}", current_mig_time, ar_version, mig)
|
151
153
|
end
|
152
154
|
done.concat(fringe)
|
153
155
|
chosen -= done
|
@@ -160,7 +162,9 @@ module Brick
|
|
160
162
|
key_type, is_4x_rails, ar_version, do_fks_last, versions_to_create)
|
161
163
|
after_fks.concat(add_fks)
|
162
164
|
current_mig_time[0] += 1.minute
|
163
|
-
versions_to_create << migration_file_write(mig_path, "create_#{
|
165
|
+
versions_to_create << migration_file_write(mig_path, "create_#{
|
166
|
+
::Brick._brick_index(tbl, :migration, separator, relations[tbl])
|
167
|
+
}", current_mig_time, ar_version, mig)
|
164
168
|
end
|
165
169
|
done.concat(chosen)
|
166
170
|
chosen.clear
|
@@ -4,12 +4,12 @@ require 'brick'
|
|
4
4
|
require 'rails/generators'
|
5
5
|
require 'rails/generators/active_record'
|
6
6
|
require 'fancy_gets'
|
7
|
-
require 'generators/brick/
|
7
|
+
require 'generators/brick/migrations_builder'
|
8
8
|
|
9
9
|
module Brick
|
10
10
|
# Auto-generates migration files
|
11
11
|
class MigrationsGenerator < ::Rails::Generators::Base
|
12
|
-
include ::Brick::
|
12
|
+
include ::Brick::MigrationsBuilder
|
13
13
|
include FancyGets
|
14
14
|
|
15
15
|
desc 'Auto-generates migration files for an existing database.'
|
@@ -25,13 +25,13 @@ module Brick
|
|
25
25
|
return
|
26
26
|
end
|
27
27
|
|
28
|
-
mig_path, is_insert_versions, is_delete_versions = ::Brick::
|
28
|
+
mig_path, is_insert_versions, is_delete_versions = ::Brick::MigrationsBuilder.check_folder
|
29
29
|
return unless mig_path
|
30
30
|
|
31
31
|
# Generate a list of tables that can be chosen
|
32
32
|
chosen = gets_list(list: tables, chosen: tables.dup)
|
33
33
|
|
34
|
-
::Brick::
|
34
|
+
::Brick::MigrationsBuilder.generate_migrations(chosen, mig_path, is_insert_versions, is_delete_versions)
|
35
35
|
end
|
36
36
|
end
|
37
37
|
end
|
@@ -3,7 +3,7 @@
|
|
3
3
|
require 'brick'
|
4
4
|
require 'rails/generators'
|
5
5
|
require 'fancy_gets'
|
6
|
-
require 'generators/brick/
|
6
|
+
require 'generators/brick/migrations_builder'
|
7
7
|
require 'generators/brick/salesforce_schema'
|
8
8
|
|
9
9
|
module Brick
|
@@ -23,7 +23,7 @@ module Brick
|
|
23
23
|
relations = nil
|
24
24
|
end_document_proc = lambda do |salesforce_tables|
|
25
25
|
# p [:end_document]
|
26
|
-
mig_path, is_insert_versions, is_delete_versions = ::Brick::
|
26
|
+
mig_path, is_insert_versions, is_delete_versions = ::Brick::MigrationsBuilder.check_folder
|
27
27
|
return unless mig_path
|
28
28
|
|
29
29
|
# Generate a list of tables that can be chosen
|
@@ -73,7 +73,7 @@ module Brick
|
|
73
73
|
}
|
74
74
|
end
|
75
75
|
# Build but do not have foreign keys established yet, and do not put version entries info the schema_migrations table
|
76
|
-
::Brick::
|
76
|
+
::Brick::MigrationsBuilder.generate_migrations(chosen, mig_path, is_insert_versions, is_delete_versions, relations,
|
77
77
|
do_fks_last: 'Separate', do_schema_migrations: false)
|
78
78
|
end
|
79
79
|
parser = Nokogiri::XML::SAX::Parser.new(::Brick::SalesforceSchema.new(end_document_proc))
|