dynamic-active-model-rails 0.11.0 → 0.13.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ff5e6fd6c5b29afc9e58143d52d5d43b39123423f1356fd4daa60cde68a5a5f1
4
- data.tar.gz: c3909cdbe0d699072ece815f55372f482c8a10c3c436c44866e1f55c24e52f8d
3
+ metadata.gz: a8515d28d9394768614fb8cd2f4cf49173ab1d5f97c905debbf3f9dc85d21f21
4
+ data.tar.gz: 33388b8956cc6106da4d6d94d0255a15b66d6156be30a49cfa9c941f31f3ed4e
5
5
  SHA512:
6
- metadata.gz: a27b214728591ccf5fe23094287bb2486d86aace729b4efb330a9d87948d9e5a42eca19966990c1a2afb5076a82282d5a48d475c6a8d90da7bf357db4fc75206
7
- data.tar.gz: 58590282a387c2fd6c1f10434e30875c43831797567726dfed8526a3c0da7f82d38bcb9a9a4e84f365637999f4bea68f7509c70b554f51536c339833664a0c8f
6
+ metadata.gz: ed7b1653b8ea5643e50b797a0cad76714958d204052ee97ba44cd10eed2e3f2103e20bcad4c16ee7277a12886ef85cceb6421a8ca41c485dec72dbac67b89fc4
7
+ data.tar.gz: 42a9e8a11125f32d8e9ea403662e232d602595c776c7829a05f7759fd14f4c9fa88822100fd92dd52c2e2188fb39c1d95082eb62c0bef16f6f5c2c17ba13beed
data/README.md CHANGED
@@ -26,6 +26,7 @@ DynamicActiveModel::Rails.configure do |config|
26
26
  # A Symbol names a database.yml entry; a URL string or config hash also works
27
27
  config.add_database :cars, :cars do |db|
28
28
  db.skip_tables 'legacy_*', /^tmp_/
29
+ db.include_tables 'cars', 'makes', 'owner*' # whitelist; default is every table
29
30
  db.foreign_key :cars, :owner_id, :owner
30
31
  db.table_class_name :status, 'StatusCode'
31
32
  end
@@ -48,6 +49,7 @@ Namespaces always end in `DB`, so a database's namespace never collides with an
48
49
  |---|---|---|
49
50
  | `add_database :cars` | `CarsDB` | `app/models/cars_db/` |
50
51
  | `add_database :grant_db` | `GrantDB` | `app/models/grant_db/` |
52
+ | `add_database :db` | `DB` | `app/models/db/` |
51
53
  | `add_database :cars, module_name: 'Inventory'` | `Inventory` | `app/models/inventory/` |
52
54
 
53
55
  The `cars_db` → `CarsDB` mapping is registered with the Rails autoloader only, not with the global inflector, so `'cars_db'.camelize` elsewhere in your app is unaffected.
@@ -59,6 +61,8 @@ The `cars_db` → `CarsDB` mapping is registered with the Rails autoloader only,
59
61
  | `connection` (2nd argument) | `nil` | `nil` shares the parent class's connection. Otherwise it's passed to `establish_connection`: a database.yml entry name (Symbol), URL or hash. |
60
62
  | `module_name:` | `"<Name>DB"` | Namespace override. |
61
63
  | `parent_class:` | `'ApplicationRecord'` | Superclass of the generated abstract base class, given as a name so it can be reloaded. |
64
+ | `extensions_path:` | `app/models/<folder>` | Directory of extension files, absolute or relative to `Rails.root`. The default folder may be absent; a configured path must exist. |
65
+ | `extensions_suffix:` | `'.ext.rb'` | Suffix of extension files. The autoloader ignores files with this suffix. |
62
66
 
63
67
  ## Extending Models
64
68
 
@@ -86,6 +90,19 @@ module CarsDB
86
90
  end
87
91
  ```
88
92
 
93
+ ## Load Hooks
94
+
95
+ Configuration that touches models at boot, like `has_paper_trail`, must be reapplied whenever the models are rebuilt. After every build, the gem runs an ActiveSupport load hook named after the database's folder. The block runs with the `DynamicActiveModel::Database` as `self`:
96
+
97
+ ```ruby
98
+ # config/initializers/paper_trail.rb
99
+ ActiveSupport.on_load(:grant_db) do
100
+ %w[users organizations].each { |table| get_model!(table).has_paper_trail }
101
+ end
102
+ ```
103
+
104
+ Avoid `Rails.application.config.after_initialize { GrantDB.database... }`. It runs once, so models rebuilt after a reload or migration would lose that setup.
105
+
89
106
  ## Lifecycle
90
107
 
91
108
  - **Lazy loading.** Nothing touches the database at boot. The first reference to a constant in a namespace builds that database's models. That's why `db:create`, `db:migrate` and `assets:precompile` don't need models to exist.
@@ -108,6 +125,37 @@ development:
108
125
  migrations_paths: db/cars_migrate
109
126
  ```
110
127
 
128
+ ## Migrating from the Setup DSL
129
+
130
+ If your app followed the core gem's [manual setup](../docs/manual-rails-setup.md):
131
+
132
+ 1. Replace `gem 'dynamic-active-model'` with `gem 'dynamic-active-model-rails'`.
133
+ 2. Delete `app/models/db.rb`. Move its settings into an initializer:
134
+
135
+ ```ruby
136
+ # config/initializers/dynamic_active_model.rb
137
+ DynamicActiveModel::Rails.configure do |config|
138
+ config.add_database :db do |db| # DB, app/models/db/
139
+ db.skip_tables 'versions'
140
+ end
141
+ end
142
+ ```
143
+
144
+ | Setup DSL | Rails gem |
145
+ |---|---|
146
+ | `parent_class ApplicationRecord` | default (omit) |
147
+ | `connection_options 'secondary'` | `add_database :db, :secondary` |
148
+ | `extensions_path '...'` | default for `app/models/db/`; otherwise `extensions_path:` |
149
+ | `extensions_suffix '.x.rb'` | `extensions_suffix:` |
150
+ | `skip_tables [...]` / `skip_table` | `db.skip_tables` |
151
+ | `foreign_key t, col, name` | `db.foreign_key t, col, name` |
152
+ | `table_class_name t, name` | `db.table_class_name t, name` |
153
+
154
+ 3. Remove the `DB` inflection and the `app/models/db` ignore from `config/application.rb`. The gem registers both.
155
+ 4. Move setup that ran after `create_models!` into `ActiveSupport.on_load(:db) { ... }`.
156
+
157
+ `.ext.rb` files stay where they are.
158
+
111
159
  ## Development
112
160
 
113
161
  From this directory:
@@ -3,8 +3,8 @@
3
3
  module DynamicActiveModel
4
4
  module Rails
5
5
  # Teaches the Rails autoloader about a database's folder: cars_db/ maps to
6
- # CarsDB (only in Zeitwerk, not the global inflector), and .ext.rb files are
7
- # left to DynamicActiveModel instead of being autoloaded.
6
+ # CarsDB (only in Zeitwerk, not the global inflector), and extension files
7
+ # (*.ext.rb by default) are left to DynamicActiveModel instead of being autoloaded.
8
8
  class AutoloaderSetup
9
9
  # @param autoloaders [Rails::Autoloaders]
10
10
  # @param definition [DatabaseDefinition]
@@ -26,7 +26,7 @@ module DynamicActiveModel
26
26
 
27
27
  # @return [String] Glob matching the database's extension files
28
28
  def extension_glob
29
- File.join(@definition.extensions_path(@root), '*.ext.rb')
29
+ File.join(@definition.extensions_path(@root), "*#{@definition.extensions_suffix}")
30
30
  end
31
31
  end
32
32
  end
@@ -20,6 +20,12 @@ module DynamicActiveModel
20
20
  # @return [Array<String, Regexp>] Tables (or patterns) to skip
21
21
  attr_reader :skipped_tables
22
22
 
23
+ # @return [Array<String, Regexp>] Tables (or patterns) to model; empty means all
24
+ attr_reader :included_tables
25
+
26
+ # @return [String] File suffix of extension files
27
+ attr_reader :extensions_suffix
28
+
23
29
  # @return [Hash] Custom relationship names by table and foreign key
24
30
  attr_reader :relationships
25
31
 
@@ -30,11 +36,18 @@ module DynamicActiveModel
30
36
  # @param connection [Symbol, String, Hash, nil] See Configuration#add_database
31
37
  # @param module_name [String, nil] Namespace override, e.g. "Inventory"
32
38
  # @param parent_class [String] Superclass for the generated base class
33
- def initialize(name, connection = nil, module_name: nil, parent_class: 'ApplicationRecord')
39
+ # @param extensions_path [String, nil] Extensions directory, absolute or relative to
40
+ # the app root; defaults to app/models/<folder>, which may be absent
41
+ # @param extensions_suffix [String] File suffix of extension files
42
+ def initialize(name, connection = nil, module_name: nil, parent_class: 'ApplicationRecord',
43
+ extensions_path: nil, extensions_suffix: '.ext.rb')
34
44
  @connection = connection
35
45
  @module_name = module_name || default_module_name(name)
36
46
  @parent_class_name = parent_class.to_s
47
+ @extensions_path = extensions_path&.to_s
48
+ @extensions_suffix = extensions_suffix
37
49
  @skipped_tables = []
50
+ @included_tables = []
38
51
  @relationships = {}
39
52
  @table_class_names = {}
40
53
  end
@@ -44,10 +57,20 @@ module DynamicActiveModel
44
57
  module_name.underscore
45
58
  end
46
59
 
60
+ # @return [Symbol] ActiveSupport load hook run after each build, e.g. :cars_db
61
+ def load_hook
62
+ folder.to_sym
63
+ end
64
+
47
65
  # @param root [Pathname, String] Application root
48
- # @return [String] Directory holding this database's .ext.rb files
66
+ # @return [String] Absolute directory holding this database's extension files
49
67
  def extensions_path(root)
50
- File.join(root.to_s, 'app', 'models', folder)
68
+ File.expand_path(@extensions_path || File.join('app', 'models', folder), root.to_s)
69
+ end
70
+
71
+ # @return [Boolean] Whether extensions_path was configured, so it must exist
72
+ def custom_extensions_path?
73
+ !@extensions_path.nil?
51
74
  end
52
75
 
53
76
  # Skips tables; strings may use * wildcards
@@ -57,6 +80,14 @@ module DynamicActiveModel
57
80
  @skipped_tables.concat(tables.flatten)
58
81
  end
59
82
 
83
+ # Models only these tables; strings may use * wildcards. ActiveRecord's
84
+ # internal tables still need their exact name.
85
+ # @param tables [Array<String, Regexp>]
86
+ # @return [void]
87
+ def include_tables(*tables)
88
+ @included_tables.concat(tables.flatten)
89
+ end
90
+
60
91
  # Names the relationship for a foreign key column
61
92
  # @param table_name [String, Symbol]
62
93
  # @param foreign_key [String, Symbol] Column name
@@ -76,11 +107,11 @@ module DynamicActiveModel
76
107
 
77
108
  private
78
109
 
79
- # :cars => "CarsDB", :grant_db => "GrantDB"
110
+ # :cars => "CarsDB", :grant_db => "GrantDB", :db => "DB"
80
111
  # @param name [Symbol, String]
81
112
  # @return [String]
82
113
  def default_module_name(name)
83
- "#{name.to_s.underscore.delete_suffix('_db').camelize}DB"
114
+ "#{name.to_s.underscore.sub(/(?:\A|_)db\z/, '').camelize}DB"
84
115
  end
85
116
  end
86
117
  end
@@ -6,6 +6,8 @@ module DynamicActiveModel
6
6
  module Rails
7
7
  # Builds one declared database's models on demand and tears them down again.
8
8
  # Loading is synchronized so concurrent first references build only once.
9
+ # After every build it runs the database's ActiveSupport load hook, so
10
+ # ActiveSupport.on_load(:cars_db) { ... } reapplies after reloads and migrations.
9
11
  class DatabaseLoader
10
12
  # @return [DatabaseDefinition]
11
13
  attr_reader :definition
@@ -41,13 +43,15 @@ module DynamicActiveModel
41
43
 
42
44
  private
43
45
 
44
- # Creates models, relationships and extensions; undoes a partial build on error
46
+ # Creates models, relationships and extensions, then runs load hooks;
47
+ # undoes a partial build on error
45
48
  # @return [void]
46
49
  def build
47
50
  @database = new_database
48
51
  @database.create_models!
49
52
  Explorer.build_relationships!(@database, definition.relationships)
50
53
  load_extensions
54
+ ActiveSupport.run_load_hooks(definition.load_hook, @database)
51
55
  rescue StandardError
52
56
  reset!
53
57
  raise
@@ -57,15 +61,22 @@ module DynamicActiveModel
57
61
  def new_database
58
62
  Database.new(namespace, definition.connection, parent_class: parent_class).tap do |database|
59
63
  definition.skipped_tables.each { |table| database.skip_table(Explorer.skip_table_matcher(table)) }
64
+ definition.included_tables.each { |table| database.include_table(Explorer.skip_table_matcher(table)) }
60
65
  definition.table_class_names.each { |table, class_name| database.table_class_name(table, class_name) }
61
66
  end
62
67
  end
63
68
 
64
- # Applies the .ext.rb files in the database's models folder
69
+ # Applies the database's extension files. The default folder is optional;
70
+ # a configured extensions_path must exist.
65
71
  # @return [void]
72
+ # @raise [DynamicActiveModel::Error] If a configured extensions_path is missing
66
73
  def load_extensions
67
74
  path = definition.extensions_path(@root)
68
- @database.update_all_models(path) if File.directory?(path)
75
+ if File.directory?(path)
76
+ @database.update_all_models(path, definition.extensions_suffix)
77
+ elsif definition.custom_extensions_path?
78
+ raise DynamicActiveModel::Error, "extensions_path #{path} for #{definition.module_name} does not exist"
79
+ end
69
80
  end
70
81
 
71
82
  # @return [Module] The namespace models are defined in
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dynamic-active-model-rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.11.0
4
+ version: 0.13.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Doug Youch
@@ -29,14 +29,14 @@ dependencies:
29
29
  requirements:
30
30
  - - '='
31
31
  - !ruby/object:Gem::Version
32
- version: 0.11.0
32
+ version: 0.13.0
33
33
  type: :runtime
34
34
  prerelease: false
35
35
  version_requirements: !ruby/object:Gem::Requirement
36
36
  requirements:
37
37
  - - '='
38
38
  - !ruby/object:Gem::Version
39
- version: 0.11.0
39
+ version: 0.13.0
40
40
  - !ruby/object:Gem::Dependency
41
41
  name: railties
42
42
  requirement: !ruby/object:Gem::Requirement