brick 1.0.228 → 1.0.230

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,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
- # # For multi-tenant databases that use a separate schema for each tenant, a single representative database schema
381
- # # can be analysed to determine the range of polymorphic classes that can be used for each association. Hopefully
382
- # # the schema chosen is one loaded with existing data that is representative of all possible polymorphic
383
- # # associations.
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 if you have a real polymorphic model configured then it is better to set the list of classes up in the
391
- # # model file itself with a line like:
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 MigrationBuilder
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_#{::Brick._brick_index(tbl, :migration, separator)}", current_mig_time, ar_version, mig)
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/migration_builder'
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::MigrationBuilder
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::MigrationBuilder.check_folder
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::MigrationBuilder.generate_migrations(chosen, mig_path, is_insert_versions, is_delete_versions)
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/migration_builder'
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::MigrationBuilder.check_folder
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::MigrationBuilder.generate_migrations(chosen, mig_path, is_insert_versions, is_delete_versions, relations,
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))
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Brick
4
4
  class SalesforceSchema < Nokogiri::XML::SAX::Document
5
- include ::Brick::MigrationBuilder
5
+ include ::Brick::MigrationsBuilder
6
6
 
7
7
  attr_reader :end_document_proc
8
8