dynamic-active-model-rails 0.11.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 +7 -0
- data/README.md +120 -0
- data/lib/dynamic-active-model/rails/autoloader_setup.rb +33 -0
- data/lib/dynamic-active-model/rails/configuration.rb +40 -0
- data/lib/dynamic-active-model/rails/database_definition.rb +87 -0
- data/lib/dynamic-active-model/rails/database_loader.rb +82 -0
- data/lib/dynamic-active-model/rails/lazy_namespace.rb +48 -0
- data/lib/dynamic-active-model/rails/railtie.rb +21 -0
- data/lib/dynamic-active-model/rails/schema_change_hook.rb +40 -0
- data/lib/dynamic-active-model/rails.rb +74 -0
- data/lib/dynamic-active-model-rails.rb +6 -0
- metadata +94 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: ff5e6fd6c5b29afc9e58143d52d5d43b39123423f1356fd4daa60cde68a5a5f1
|
|
4
|
+
data.tar.gz: c3909cdbe0d699072ece815f55372f482c8a10c3c436c44866e1f55c24e52f8d
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: a27b214728591ccf5fe23094287bb2486d86aace729b4efb330a9d87948d9e5a42eca19966990c1a2afb5076a82282d5a48d475c6a8d90da7bf357db4fc75206
|
|
7
|
+
data.tar.gz: 58590282a387c2fd6c1f10434e30875c43831797567726dfed8526a3c0da7f82d38bcb9a9a4e84f365637999f4bea68f7509c70b554f51536c339833664a0c8f
|
data/README.md
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# dynamic-active-model-rails
|
|
2
|
+
|
|
3
|
+
Rails integration for [dynamic-active-model](https://github.com/dougyouch/dynamic-active-model). Declare your databases in one initializer, and their tables become ActiveRecord models in a `<Name>DB` namespace. Models are built on first use, rebuilt after migrations, and reloaded with the rest of the app in development.
|
|
4
|
+
|
|
5
|
+
Requires Rails 7.1+ and Ruby 3.2+.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
# Gemfile
|
|
11
|
+
gem 'dynamic-active-model-rails'
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
`dynamic-active-model-rails` is released in lockstep with `dynamic-active-model` and pins the same version.
|
|
15
|
+
|
|
16
|
+
## Configuration
|
|
17
|
+
|
|
18
|
+
```ruby
|
|
19
|
+
# config/initializers/dynamic_active_model.rb
|
|
20
|
+
DynamicActiveModel::Rails.configure do |config|
|
|
21
|
+
# No connection: models subclass ApplicationRecord and share its connection pool
|
|
22
|
+
config.add_database :grant_db do |db|
|
|
23
|
+
db.skip_tables 'versions'
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# A Symbol names a database.yml entry; a URL string or config hash also works
|
|
27
|
+
config.add_database :cars, :cars do |db|
|
|
28
|
+
db.skip_tables 'legacy_*', /^tmp_/
|
|
29
|
+
db.foreign_key :cars, :owner_id, :owner
|
|
30
|
+
db.table_class_name :status, 'StatusCode'
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
```ruby
|
|
36
|
+
GrantDB::User.find(1)
|
|
37
|
+
CarsDB::Car.where(make: 'Honda')
|
|
38
|
+
CarsDB.models # => [CarsDB::Car, CarsDB::Make, ...]
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
`schema_migrations` and `ar_internal_metadata` are always skipped.
|
|
42
|
+
|
|
43
|
+
### Naming
|
|
44
|
+
|
|
45
|
+
Namespaces always end in `DB`, so a database's namespace never collides with an application class:
|
|
46
|
+
|
|
47
|
+
| Declaration | Namespace | Folder |
|
|
48
|
+
|---|---|---|
|
|
49
|
+
| `add_database :cars` | `CarsDB` | `app/models/cars_db/` |
|
|
50
|
+
| `add_database :grant_db` | `GrantDB` | `app/models/grant_db/` |
|
|
51
|
+
| `add_database :cars, module_name: 'Inventory'` | `Inventory` | `app/models/inventory/` |
|
|
52
|
+
|
|
53
|
+
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.
|
|
54
|
+
|
|
55
|
+
### Options
|
|
56
|
+
|
|
57
|
+
| Option | Default | Description |
|
|
58
|
+
|---|---|---|
|
|
59
|
+
| `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
|
+
| `module_name:` | `"<Name>DB"` | Namespace override. |
|
|
61
|
+
| `parent_class:` | `'ApplicationRecord'` | Superclass of the generated abstract base class, given as a name so it can be reloaded. |
|
|
62
|
+
|
|
63
|
+
## Extending Models
|
|
64
|
+
|
|
65
|
+
Add one `<table_name>.ext.rb` file per table to the database's folder:
|
|
66
|
+
|
|
67
|
+
```ruby
|
|
68
|
+
# app/models/cars_db/cars.ext.rb
|
|
69
|
+
update_model do
|
|
70
|
+
scope :by_make, ->(name) { joins(:make).where(makes: { name: name }) }
|
|
71
|
+
|
|
72
|
+
def display_name
|
|
73
|
+
"#{make.name} #{model}"
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
The autoloader ignores `.ext.rb` files. Other `.rb` files in the folder autoload into the namespace as usual, so you can keep related classes next to the models:
|
|
79
|
+
|
|
80
|
+
```ruby
|
|
81
|
+
# app/models/cars_db/search.rb
|
|
82
|
+
module CarsDB
|
|
83
|
+
class Search
|
|
84
|
+
def self.for_make(name) = CarsDB::Car.by_make(name)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Lifecycle
|
|
90
|
+
|
|
91
|
+
- **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.
|
|
92
|
+
- **Eager loading.** With `config.eager_load` (production), every database's models are built at boot, along with the rest of the app.
|
|
93
|
+
- **Migrations.** After migrations run or a schema is loaded (`db:migrate`, `db:rollback`, `db:prepare`, `db:schema:load`, `maintain_test_schema!`), models are reset and rebuild on next use. That means `bin/rails db:prepare` followed by seeding works in one process.
|
|
94
|
+
- **Reloading.** In development, models are rebuilt whenever the app reloads, including after edits to `.ext.rb` files or to anything under `db/`, such as a migration rewriting `db/schema.rb`.
|
|
95
|
+
|
|
96
|
+
## Migrations
|
|
97
|
+
|
|
98
|
+
Migrations stay standard Rails. The gem reads the schema; it never owns it. For a secondary database, give its database.yml entry a `migrations_paths`:
|
|
99
|
+
|
|
100
|
+
```yaml
|
|
101
|
+
development:
|
|
102
|
+
primary:
|
|
103
|
+
<<: *default
|
|
104
|
+
database: storage/development.sqlite3
|
|
105
|
+
cars:
|
|
106
|
+
<<: *default
|
|
107
|
+
database: storage/cars.sqlite3
|
|
108
|
+
migrations_paths: db/cars_migrate
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Development
|
|
112
|
+
|
|
113
|
+
From this directory:
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
bundle install
|
|
117
|
+
bundle exec rspec
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
The specs boot a small Rails app in `spec/dummy` that has a primary and a `cars` SQLite database.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DynamicActiveModel
|
|
4
|
+
module Rails
|
|
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.
|
|
8
|
+
class AutoloaderSetup
|
|
9
|
+
# @param autoloaders [Rails::Autoloaders]
|
|
10
|
+
# @param definition [DatabaseDefinition]
|
|
11
|
+
# @param root [Pathname, String] Application root
|
|
12
|
+
def initialize(autoloaders, definition, root)
|
|
13
|
+
@autoloaders = autoloaders
|
|
14
|
+
@definition = definition
|
|
15
|
+
@root = root
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Must run before the main autoloader is set up, i.e. from an initializer
|
|
19
|
+
# @return [void]
|
|
20
|
+
def apply
|
|
21
|
+
@autoloaders.main.inflector.inflect(@definition.folder => @definition.module_name)
|
|
22
|
+
@autoloaders.main.ignore(extension_glob)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
# @return [String] Glob matching the database's extension files
|
|
28
|
+
def extension_glob
|
|
29
|
+
File.join(@definition.extensions_path(@root), '*.ext.rb')
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DynamicActiveModel
|
|
4
|
+
module Rails
|
|
5
|
+
# Collects the databases declared in DynamicActiveModel::Rails.configure
|
|
6
|
+
class Configuration
|
|
7
|
+
# @return [Array<DatabaseDefinition>] Declared databases, in order
|
|
8
|
+
attr_reader :definitions
|
|
9
|
+
|
|
10
|
+
def initialize
|
|
11
|
+
@definitions = []
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Declares a database whose tables become models in a <Name>DB namespace
|
|
15
|
+
# @param name [Symbol, String] Database name, e.g. :cars => CarsDB, app/models/cars_db/
|
|
16
|
+
# @param connection [Symbol, String, Hash, nil] database.yml entry name, URL or
|
|
17
|
+
# config hash; nil shares the parent class's connection
|
|
18
|
+
# @param ** [Hash] DatabaseDefinition options (module_name:, parent_class:)
|
|
19
|
+
# @yieldparam definition [DatabaseDefinition] For table-level settings
|
|
20
|
+
# @return [DatabaseDefinition]
|
|
21
|
+
# @raise [ArgumentError] If the namespace is already declared
|
|
22
|
+
def add_database(name, connection = nil, **)
|
|
23
|
+
definition = DatabaseDefinition.new(name, connection, **)
|
|
24
|
+
raise ArgumentError, "#{definition.module_name} is already declared" if declared?(definition.module_name)
|
|
25
|
+
|
|
26
|
+
yield definition if block_given?
|
|
27
|
+
@definitions << definition
|
|
28
|
+
definition
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
# @param module_name [String]
|
|
34
|
+
# @return [Boolean] Whether a database already uses the namespace
|
|
35
|
+
def declared?(module_name)
|
|
36
|
+
@definitions.any? { |definition| definition.module_name == module_name }
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'active_support/core_ext/string/inflections'
|
|
4
|
+
|
|
5
|
+
module DynamicActiveModel
|
|
6
|
+
module Rails
|
|
7
|
+
# Settings for one declared database: its connection, namespace, folder and
|
|
8
|
+
# table-level options. Names always end in DB so a namespace never collides
|
|
9
|
+
# with an application class: :cars => CarsDB in app/models/cars_db/.
|
|
10
|
+
class DatabaseDefinition
|
|
11
|
+
# @return [Symbol, String, Hash, nil] Connection passed to establish_connection
|
|
12
|
+
attr_reader :connection
|
|
13
|
+
|
|
14
|
+
# @return [String] Name of the namespace module, e.g. "CarsDB"
|
|
15
|
+
attr_reader :module_name
|
|
16
|
+
|
|
17
|
+
# @return [String] Name of the base class's superclass, constantized at load time
|
|
18
|
+
attr_reader :parent_class_name
|
|
19
|
+
|
|
20
|
+
# @return [Array<String, Regexp>] Tables (or patterns) to skip
|
|
21
|
+
attr_reader :skipped_tables
|
|
22
|
+
|
|
23
|
+
# @return [Hash] Custom relationship names by table and foreign key
|
|
24
|
+
attr_reader :relationships
|
|
25
|
+
|
|
26
|
+
# @return [Hash] Custom class names by table name
|
|
27
|
+
attr_reader :table_class_names
|
|
28
|
+
|
|
29
|
+
# @param name [Symbol, String] Database name
|
|
30
|
+
# @param connection [Symbol, String, Hash, nil] See Configuration#add_database
|
|
31
|
+
# @param module_name [String, nil] Namespace override, e.g. "Inventory"
|
|
32
|
+
# @param parent_class [String] Superclass for the generated base class
|
|
33
|
+
def initialize(name, connection = nil, module_name: nil, parent_class: 'ApplicationRecord')
|
|
34
|
+
@connection = connection
|
|
35
|
+
@module_name = module_name || default_module_name(name)
|
|
36
|
+
@parent_class_name = parent_class.to_s
|
|
37
|
+
@skipped_tables = []
|
|
38
|
+
@relationships = {}
|
|
39
|
+
@table_class_names = {}
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# @return [String] Folder under app/models for extensions, e.g. "cars_db"
|
|
43
|
+
def folder
|
|
44
|
+
module_name.underscore
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# @param root [Pathname, String] Application root
|
|
48
|
+
# @return [String] Directory holding this database's .ext.rb files
|
|
49
|
+
def extensions_path(root)
|
|
50
|
+
File.join(root.to_s, 'app', 'models', folder)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Skips tables; strings may use * wildcards
|
|
54
|
+
# @param tables [Array<String, Regexp>]
|
|
55
|
+
# @return [void]
|
|
56
|
+
def skip_tables(*tables)
|
|
57
|
+
@skipped_tables.concat(tables.flatten)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Names the relationship for a foreign key column
|
|
61
|
+
# @param table_name [String, Symbol]
|
|
62
|
+
# @param foreign_key [String, Symbol] Column name
|
|
63
|
+
# @param relationship_name [String, Symbol]
|
|
64
|
+
# @return [void]
|
|
65
|
+
def foreign_key(table_name, foreign_key, relationship_name)
|
|
66
|
+
(@relationships[table_name.to_s] ||= {})[foreign_key.to_s] = relationship_name.to_s
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Overrides the model class name for a table
|
|
70
|
+
# @param table_name [String, Symbol]
|
|
71
|
+
# @param class_name [String]
|
|
72
|
+
# @return [void]
|
|
73
|
+
def table_class_name(table_name, class_name)
|
|
74
|
+
@table_class_names[table_name.to_s] = class_name.to_s
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
private
|
|
78
|
+
|
|
79
|
+
# :cars => "CarsDB", :grant_db => "GrantDB"
|
|
80
|
+
# @param name [Symbol, String]
|
|
81
|
+
# @return [String]
|
|
82
|
+
def default_module_name(name)
|
|
83
|
+
"#{name.to_s.underscore.delete_suffix('_db').camelize}DB"
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'monitor'
|
|
4
|
+
|
|
5
|
+
module DynamicActiveModel
|
|
6
|
+
module Rails
|
|
7
|
+
# Builds one declared database's models on demand and tears them down again.
|
|
8
|
+
# Loading is synchronized so concurrent first references build only once.
|
|
9
|
+
class DatabaseLoader
|
|
10
|
+
# @return [DatabaseDefinition]
|
|
11
|
+
attr_reader :definition
|
|
12
|
+
|
|
13
|
+
# @param definition [DatabaseDefinition]
|
|
14
|
+
# @param root [Pathname, String] Application root, for the extensions folder
|
|
15
|
+
def initialize(definition, root)
|
|
16
|
+
@definition = definition
|
|
17
|
+
@root = root
|
|
18
|
+
@monitor = Monitor.new
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Builds the models unless already built
|
|
22
|
+
# @return [DynamicActiveModel::Database]
|
|
23
|
+
def load!
|
|
24
|
+
@monitor.synchronize { build unless @database }
|
|
25
|
+
@database
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# @return [Boolean] Whether models are built (or being built)
|
|
29
|
+
def loaded?
|
|
30
|
+
!@database.nil?
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Removes the built models so the next #load! rebuilds them
|
|
34
|
+
# @return [void]
|
|
35
|
+
def reset!
|
|
36
|
+
@monitor.synchronize do
|
|
37
|
+
@database&.reset!
|
|
38
|
+
@database = nil
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
# Creates models, relationships and extensions; undoes a partial build on error
|
|
45
|
+
# @return [void]
|
|
46
|
+
def build
|
|
47
|
+
@database = new_database
|
|
48
|
+
@database.create_models!
|
|
49
|
+
Explorer.build_relationships!(@database, definition.relationships)
|
|
50
|
+
load_extensions
|
|
51
|
+
rescue StandardError
|
|
52
|
+
reset!
|
|
53
|
+
raise
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# @return [DynamicActiveModel::Database] A database configured from the definition
|
|
57
|
+
def new_database
|
|
58
|
+
Database.new(namespace, definition.connection, parent_class: parent_class).tap do |database|
|
|
59
|
+
definition.skipped_tables.each { |table| database.skip_table(Explorer.skip_table_matcher(table)) }
|
|
60
|
+
definition.table_class_names.each { |table, class_name| database.table_class_name(table, class_name) }
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Applies the .ext.rb files in the database's models folder
|
|
65
|
+
# @return [void]
|
|
66
|
+
def load_extensions
|
|
67
|
+
path = definition.extensions_path(@root)
|
|
68
|
+
@database.update_all_models(path) if File.directory?(path)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# @return [Module] The namespace models are defined in
|
|
72
|
+
def namespace
|
|
73
|
+
Object.const_get(definition.module_name)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# @return [Class] The base class's superclass, resolved now so it can be reloaded
|
|
77
|
+
def parent_class
|
|
78
|
+
Object.const_get(definition.parent_class_name)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DynamicActiveModel
|
|
4
|
+
module Rails
|
|
5
|
+
# Extended into a database's namespace module (e.g. CarsDB). The first
|
|
6
|
+
# reference to a missing constant builds the database's models.
|
|
7
|
+
module LazyNamespace
|
|
8
|
+
# @return [DatabaseLoader]
|
|
9
|
+
attr_accessor :dynamic_active_model_loader
|
|
10
|
+
|
|
11
|
+
# Defines (or reuses) a top-level namespace module and makes it lazy
|
|
12
|
+
# @param module_name [String]
|
|
13
|
+
# @param loader [DatabaseLoader]
|
|
14
|
+
# @return [Module]
|
|
15
|
+
def self.define(module_name, loader)
|
|
16
|
+
namespace =
|
|
17
|
+
if Object.const_defined?(module_name, false)
|
|
18
|
+
Object.const_get(module_name, false)
|
|
19
|
+
else
|
|
20
|
+
Object.const_set(module_name, Module.new)
|
|
21
|
+
end
|
|
22
|
+
namespace.extend(self)
|
|
23
|
+
namespace.dynamic_active_model_loader = loader
|
|
24
|
+
namespace
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# @return [DynamicActiveModel::Database] The built database
|
|
28
|
+
def database
|
|
29
|
+
dynamic_active_model_loader.load!
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# @return [Array<Class>] Every model in the namespace
|
|
33
|
+
def models
|
|
34
|
+
database.models
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Builds the models on first reference, then retries the lookup
|
|
38
|
+
# @param name [Symbol]
|
|
39
|
+
# @return [Object]
|
|
40
|
+
def const_missing(name)
|
|
41
|
+
return super if dynamic_active_model_loader.loaded?
|
|
42
|
+
|
|
43
|
+
dynamic_active_model_loader.load!
|
|
44
|
+
const_defined?(name, false) ? const_get(name, false) : super
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DynamicActiveModel
|
|
4
|
+
module Rails
|
|
5
|
+
# Hooks declared databases into Rails: eager loading, code reloading and
|
|
6
|
+
# schema changes all rebuild models instead of leaving them stale.
|
|
7
|
+
class Railtie < ::Rails::Railtie
|
|
8
|
+
config.eager_load_namespaces << DynamicActiveModel::Rails
|
|
9
|
+
|
|
10
|
+
initializer 'dynamic_active_model.schema_change_hook' do
|
|
11
|
+
ActiveSupport.on_load(:active_record) { DynamicActiveModel::Rails::SchemaChangeHook.install }
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
initializer 'dynamic_active_model.reloader' do |app|
|
|
15
|
+
app.reloader.before_class_unload { DynamicActiveModel::Rails.reset! }
|
|
16
|
+
# a migration rewrites db/schema.rb (or structure.sql); reload so models rebuild
|
|
17
|
+
app.config.watchable_dirs[app.root.join('db').to_s] = %i[rb sql]
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DynamicActiveModel
|
|
4
|
+
module Rails
|
|
5
|
+
# Resets every database's models after migrations run or a schema is loaded,
|
|
6
|
+
# so models referenced later in the same process see the new schema. Covers
|
|
7
|
+
# db:migrate, db:rollback, db:prepare, db:schema:load and maintain_test_schema!.
|
|
8
|
+
module SchemaChangeHook
|
|
9
|
+
# Prepended to ActiveRecord::Migrator
|
|
10
|
+
module Migrator
|
|
11
|
+
def migrate
|
|
12
|
+
super
|
|
13
|
+
ensure
|
|
14
|
+
DynamicActiveModel::Rails.reset!
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def run
|
|
18
|
+
super
|
|
19
|
+
ensure
|
|
20
|
+
DynamicActiveModel::Rails.reset!
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Prepended to ActiveRecord::Tasks::DatabaseTasks' singleton class
|
|
25
|
+
module DatabaseTasks
|
|
26
|
+
def load_schema(...)
|
|
27
|
+
super
|
|
28
|
+
ensure
|
|
29
|
+
DynamicActiveModel::Rails.reset!
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# @return [void]
|
|
34
|
+
def self.install
|
|
35
|
+
ActiveRecord::Migrator.prepend(Migrator)
|
|
36
|
+
ActiveRecord::Tasks::DatabaseTasks.singleton_class.prepend(DatabaseTasks)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DynamicActiveModel
|
|
4
|
+
# Rails integration for DynamicActiveModel. Declare each database once in an
|
|
5
|
+
# initializer; its models are built lazily on first reference, rebuilt after
|
|
6
|
+
# migrations, and reloaded with the rest of the app in development.
|
|
7
|
+
#
|
|
8
|
+
# @example config/initializers/dynamic_active_model.rb
|
|
9
|
+
# DynamicActiveModel::Rails.configure do |config|
|
|
10
|
+
# config.add_database :grant_db # ApplicationRecord's connection
|
|
11
|
+
# config.add_database :cars, :cars do |db| # "cars" entry in database.yml
|
|
12
|
+
# db.skip_tables 'legacy_*'
|
|
13
|
+
# end
|
|
14
|
+
# end
|
|
15
|
+
#
|
|
16
|
+
# GrantDB::User.first
|
|
17
|
+
# CarsDB::Car.where(make: 'Honda')
|
|
18
|
+
module Rails
|
|
19
|
+
autoload :AutoloaderSetup, 'dynamic-active-model/rails/autoloader_setup'
|
|
20
|
+
autoload :Configuration, 'dynamic-active-model/rails/configuration'
|
|
21
|
+
autoload :DatabaseDefinition, 'dynamic-active-model/rails/database_definition'
|
|
22
|
+
autoload :DatabaseLoader, 'dynamic-active-model/rails/database_loader'
|
|
23
|
+
autoload :LazyNamespace, 'dynamic-active-model/rails/lazy_namespace'
|
|
24
|
+
autoload :SchemaChangeHook, 'dynamic-active-model/rails/schema_change_hook'
|
|
25
|
+
|
|
26
|
+
class << self
|
|
27
|
+
# Declares databases. Each new database gets its namespace module and
|
|
28
|
+
# autoloader settings immediately, so call this from an initializer.
|
|
29
|
+
# @yieldparam config [Configuration]
|
|
30
|
+
# @return [Configuration]
|
|
31
|
+
def configure
|
|
32
|
+
yield configuration
|
|
33
|
+
configuration.definitions.each { |definition| register(definition) }
|
|
34
|
+
configuration
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# @return [Configuration] The process-wide configuration
|
|
38
|
+
def configuration
|
|
39
|
+
@configuration ||= Configuration.new
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# @return [Array<DatabaseLoader>] A loader per declared database
|
|
43
|
+
def loaders
|
|
44
|
+
@loaders ||= []
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Builds every database's models. Rails calls this when eager loading.
|
|
48
|
+
# @return [void]
|
|
49
|
+
def eager_load!
|
|
50
|
+
loaders.each(&:load!)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Forgets every database's models so they rebuild on next reference
|
|
54
|
+
# @return [void]
|
|
55
|
+
def reset!
|
|
56
|
+
loaders.each(&:reset!)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
# Sets up the namespace and autoloader for a definition not seen before
|
|
62
|
+
# @param definition [DatabaseDefinition]
|
|
63
|
+
# @return [void]
|
|
64
|
+
def register(definition)
|
|
65
|
+
return if loaders.any? { |loader| loader.definition.equal?(definition) }
|
|
66
|
+
|
|
67
|
+
loader = DatabaseLoader.new(definition, ::Rails.root)
|
|
68
|
+
LazyNamespace.define(definition.module_name, loader)
|
|
69
|
+
AutoloaderSetup.new(::Rails.autoloaders, definition, ::Rails.root).apply
|
|
70
|
+
loaders << loader
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: dynamic-active-model-rails
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.11.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Doug Youch
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: activerecord
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '7.1'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '7.1'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: dynamic-active-model
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - '='
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: 0.11.0
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - '='
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: 0.11.0
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: railties
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - ">="
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '7.1'
|
|
47
|
+
type: :runtime
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - ">="
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '7.1'
|
|
54
|
+
description: 'Railtie for dynamic-active-model: declare databases in one initializer
|
|
55
|
+
and get lazily built, reloadable models in CarsDB-style namespaces, with extension
|
|
56
|
+
files in app/models/cars_db/ and automatic rebuilds after migrations.'
|
|
57
|
+
email: dougyouch@gmail.com
|
|
58
|
+
executables: []
|
|
59
|
+
extensions: []
|
|
60
|
+
extra_rdoc_files: []
|
|
61
|
+
files:
|
|
62
|
+
- README.md
|
|
63
|
+
- lib/dynamic-active-model-rails.rb
|
|
64
|
+
- lib/dynamic-active-model/rails.rb
|
|
65
|
+
- lib/dynamic-active-model/rails/autoloader_setup.rb
|
|
66
|
+
- lib/dynamic-active-model/rails/configuration.rb
|
|
67
|
+
- lib/dynamic-active-model/rails/database_definition.rb
|
|
68
|
+
- lib/dynamic-active-model/rails/database_loader.rb
|
|
69
|
+
- lib/dynamic-active-model/rails/lazy_namespace.rb
|
|
70
|
+
- lib/dynamic-active-model/rails/railtie.rb
|
|
71
|
+
- lib/dynamic-active-model/rails/schema_change_hook.rb
|
|
72
|
+
homepage: https://github.com/dougyouch/dynamic-active-model/tree/master/dynamic-active-model-rails
|
|
73
|
+
licenses:
|
|
74
|
+
- MIT
|
|
75
|
+
metadata:
|
|
76
|
+
rubygems_mfa_required: 'true'
|
|
77
|
+
rdoc_options: []
|
|
78
|
+
require_paths:
|
|
79
|
+
- lib
|
|
80
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
81
|
+
requirements:
|
|
82
|
+
- - ">="
|
|
83
|
+
- !ruby/object:Gem::Version
|
|
84
|
+
version: '3.2'
|
|
85
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
86
|
+
requirements:
|
|
87
|
+
- - ">="
|
|
88
|
+
- !ruby/object:Gem::Version
|
|
89
|
+
version: '0'
|
|
90
|
+
requirements: []
|
|
91
|
+
rubygems_version: 4.0.20
|
|
92
|
+
specification_version: 4
|
|
93
|
+
summary: Rails integration for dynamic-active-model
|
|
94
|
+
test_files: []
|