cell 0.1.1 → 0.1.2
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/.gitignore +1 -0
- data/README.md +125 -49
- data/Rakefile +2 -1
- data/cell.gemspec +1 -1
- data/lib/cell/active_job.rb +32 -0
- data/lib/cell/clone_schema.rb +18 -16
- data/lib/cell/console.rb +9 -15
- data/lib/cell/context.rb +48 -27
- data/lib/cell/meta.rb +87 -0
- data/lib/cell/migration.rb +131 -44
- data/lib/cell/model_extensions.rb +85 -0
- data/lib/cell/railtie.rb +17 -3
- data/lib/cell/sanity_check.rb +21 -23
- data/lib/cell/schema.rb +2 -13
- data/lib/cell/tasks/cell.rake +18 -11
- data/lib/cell/tenant.rb +55 -0
- data/lib/cell/url_options.rb +26 -0
- data/lib/cell/version.rb +1 -1
- data/lib/cell.rb +1 -6
- metadata +9 -11
- data/lib/cell/configure.rb +0 -70
- data/lib/cell/data_directory.rb +0 -42
- data/lib/cell/model.rb +0 -37
- data/lib/cell/sidekiq.rb +0 -58
- data/lib/cell/with_schema.rb +0 -30
- data/lib/dump/tenant_routing_manager.rb +0 -25
- data/lib/dump/tenant_url_options.rb +0 -17
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 688c58ded30105abbc900e3b92d524549b860812
|
4
|
+
data.tar.gz: e176f9fc293ad7027e53052a09561e26e11f1091
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ec3f3baee67fa98496dabaf50554f5bcfe4b230d0ae6c5d5eba21d00635e5cfeaed3156e233cce8e31289ae73a012df07090649c93e7d7b23bbc8aa95384901d
|
7
|
+
data.tar.gz: 89ef969c710585e9afb2b43ae8ef8eecc343213586e38eaae9a6a463aef45fdf4033f044781243e949fec595821c8ac2f0466c8e00d3af30ef65c182e227ed16
|
data/.gitignore
CHANGED
data/README.md
CHANGED
@@ -1,42 +1,38 @@
|
|
1
1
|
# Cell
|
2
2
|
|
3
|
-
Cell provides tenancy and customer isolation for Rails. It tries to be a
|
4
|
-
|
3
|
+
Cell provides tenancy and customer isolation for Rails. It tries to be a simpler and more thorough
|
4
|
+
implementation of things like Apartment.
|
5
5
|
|
6
|
-
The core of Cell was written years ago for Rails 1.2, before Apartment was
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
more generalized gem. Things are a bit messy now.
|
6
|
+
The core of Cell was written years ago for Rails 1.2, before Apartment was available, and has been
|
7
|
+
developed inside an application ever since. We've aggressively moved it along with the ecosystem,
|
8
|
+
and it still feels more modern than other implementations we've used. We're in the process of
|
9
|
+
making it a more generalized gem. Things are a bit messy now.
|
11
10
|
|
12
|
-
Cell tries to hook into Rails at the highest level available (e.g., ActiveJob
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
level security.)
|
11
|
+
Cell tries to hook into Rails at the highest level available (e.g., ActiveJob instead of a
|
12
|
+
particular job adapter), except for one place: PostgreSQL. I think PostgreSQL is the only widely
|
13
|
+
deployed database with the features required to make a reasonable implementation (namely: schemas,
|
14
|
+
roles, row and column level security.)
|
17
15
|
|
18
|
-
**Cell aggressively uses Ruby 2.3, PostgreSQL 9.6 and Rails 5**. This means
|
19
|
-
|
20
|
-
existing application on an older stack.
|
16
|
+
**Cell aggressively uses Ruby 2.3, PostgreSQL 9.6 and Rails 5**. This means it's probably more
|
17
|
+
appropriate for greenfield projects than bolting on to an existing application on an older stack.
|
21
18
|
|
22
|
-
Cell moves forward quickly: Don't expect to be able to hang back on an old
|
23
|
-
|
24
|
-
|
25
|
-
Rails versions.
|
19
|
+
Cell moves forward quickly: Don't expect to be able to hang back on an old version of Rails and get
|
20
|
+
new features in Cell. We version accordingly not to break things, but we don't back-port to old
|
21
|
+
Rubies, old PostgreSQLs, or old Rails versions. We'll totally change how it works overnight.
|
26
22
|
|
27
|
-
The aforementioned Apartment gem looks like perfectly viable solution for
|
28
|
-
|
23
|
+
The aforementioned Apartment gem looks like perfectly viable solution for projects with other
|
24
|
+
constraints.
|
29
25
|
|
30
26
|
What it handles:
|
31
27
|
|
32
|
-
[x] ActiveRecord
|
33
|
-
[
|
34
|
-
[ ]
|
35
|
-
[
|
36
|
-
[ ]
|
37
|
-
[ ]
|
38
|
-
[
|
39
|
-
[x] Rake
|
28
|
+
- [x] ActiveRecord models
|
29
|
+
- [x] Migrations: including shared and global tables
|
30
|
+
- [ ] ActionController: An integration point for you
|
31
|
+
- [x] ActiveJob: Jobs are executed in the proper context
|
32
|
+
- [ ] ActionMailer: Your URIs will work
|
33
|
+
- [ ] ActionView: Caching is automatically keyed on the Cell
|
34
|
+
- [ ] Redis: optional, via redis-namespace
|
35
|
+
- [x] Rake: tasks which run over each Cell
|
40
36
|
|
41
37
|
The rest of this README will be updated as the code is extracted.
|
42
38
|
|
@@ -48,38 +44,118 @@ Add this line to your application's Gemfile:
|
|
48
44
|
gem 'cell'
|
49
45
|
```
|
50
46
|
|
51
|
-
|
47
|
+
## Tests
|
52
48
|
|
53
|
-
|
49
|
+
The application Cell was ripped out of had heavy test-coverage of the functionality Cell provides,
|
50
|
+
but at the wrong level (e.g., our model, not the Cell behavior which this became). It's proven
|
51
|
+
impossible to extract anything workable out of these into this gem, but we're working on starting
|
52
|
+
from scratch.
|
54
53
|
|
55
|
-
|
54
|
+
# How it Works
|
56
55
|
|
57
|
-
|
56
|
+
The most complicated interaction Cell has with Rails exist with ActiveRecord and its migrations.
|
57
|
+
First, what happens in your PostgreSQL database:
|
58
58
|
|
59
|
-
|
59
|
+
Global tables are generated in the default schema, which we'll just call "public", and they stay
|
60
|
+
there. This is what you're used to.
|
61
|
+
|
62
|
+
Per-tenant tables are redirected to a schema called "cell_prototype".
|
63
|
+
|
64
|
+
Both of these are versioned by 'public.schema_migrations'. e.g., the prototype and public schemas
|
65
|
+
are always considered to be the same version.
|
66
|
+
|
67
|
+
When you create a new tenant, the `cell_prototype` schema is copied to a new schema for the tenant,
|
68
|
+
and the tenant gets its own 'schema_migrations' table, copied from global.
|
69
|
+
|
70
|
+
When you 'db:migrate', un-ran migrations start populating public and cell_prototype, but not
|
71
|
+
tenants.
|
72
|
+
|
73
|
+
To update existing tenants after a 'db:migrate', you need to run 'cell:db:migrate', which will run
|
74
|
+
the migrations needed to bring each tenant up to date. There are an entire suite of `cell:` rake
|
75
|
+
tasks that just run the normal task over each tenant. Check out `rake -T | grep cell:`
|
60
76
|
|
61
|
-
|
62
|
-
|
63
|
-
Cell
|
64
|
-
workable out of these into this gem, but we're working on starting from
|
65
|
-
scratch.
|
77
|
+
Cell activates a tenant by adjusting 'schema_search_path'. The way to use this is
|
78
|
+
`YourModel.use {|block }`. Note that `Cell::Model` ends up aliased to `YourModel`, so
|
79
|
+
`Cell::Model.find(...).use` works as well.
|
66
80
|
|
81
|
+
There are a lot of tricks Cell has to use re: thread safety and the query cache to make this act
|
82
|
+
normally.
|
83
|
+
|
84
|
+
In the development mode console, Cell will activate the first tenant for you before you get a REPL.
|
67
85
|
|
68
86
|
## Usage
|
69
87
|
|
70
|
-
|
88
|
+
First, read the [How it Works](#how-it-works) section. You need to know what's going on in case
|
89
|
+
something breaks.
|
90
|
+
|
91
|
+
Please don't use cell unless you're OK with the idea of digging into the code when things go wrong.
|
92
|
+
It's not a drop-in thing yet.
|
93
|
+
|
94
|
+
Feel free to open PRs or tickets, though, but I'd rather see "We ended up having to use something
|
95
|
+
else because Cell doesn't handle this" than "Cell must handle this".
|
96
|
+
|
97
|
+
I'm now committed to keeping the following section up-to-date, as the only source of piss-poor
|
98
|
+
documentation.
|
99
|
+
|
100
|
+
First, use `cell` in your `Gemfile` as stated earlier.
|
101
|
+
|
102
|
+
First, you need a migration that does the following, and only the following:
|
71
103
|
|
72
|
-
|
104
|
+
```ruby
|
105
|
+
class InitializeCell < ActiveRecord::Migration[5.0]
|
106
|
+
def change
|
107
|
+
initialize_cell!
|
108
|
+
end
|
109
|
+
end
|
110
|
+
```
|
111
|
+
|
112
|
+
Then you need a migration that creates your tenanted model:
|
113
|
+
```ruby
|
114
|
+
class CreateAccounts < ActiveRecord::Migration[5.0]
|
115
|
+
def change
|
116
|
+
global do
|
117
|
+
create_table :accounts do |t|
|
118
|
+
# ...
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
```
|
124
|
+
|
125
|
+
We do the configuration in the model so it can be updated if you're running the (very useful)
|
126
|
+
clusterfuck that is Spring: your settings will update in development.
|
127
|
+
|
128
|
+
```ruby
|
129
|
+
class Account < ApplicationRecord
|
130
|
+
extend Cell::Tenant
|
131
|
+
end
|
132
|
+
```
|
133
|
+
|
134
|
+
From there on out, most of the weird shit dealing with Cell will be in migrations. By default, it
|
135
|
+
assumes everything you do in a migration is per-tenant. If you want to escape this, or create
|
136
|
+
shared/global tables, you need to wrap it in a global block.
|
137
|
+
|
138
|
+
```ruby
|
139
|
+
class TestMigration < ActiveRecord::Migration[5.0]
|
140
|
+
def change
|
141
|
+
# This will exist in each tenant
|
142
|
+
create_table :users do |t|
|
143
|
+
t.string :name, null: false
|
144
|
+
end
|
145
|
+
|
146
|
+
# This is global, and will be shared across all tenants
|
147
|
+
global do
|
148
|
+
create_table :capabilities do |t|
|
149
|
+
t.string :name, null: false
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
```
|
73
155
|
|
74
|
-
|
75
|
-
|
76
|
-
to experiment.
|
156
|
+
`rake db:migrate` updates the global and prototype schema, `rake cell:db:migrate` will run the
|
157
|
+
non-global blocks across all existing tenants.
|
77
158
|
|
78
|
-
To install this gem onto your local machine, run `bundle exec rake install`. To
|
79
|
-
release a new version, update the version number in `version.rb`, and then
|
80
|
-
run `bundle exec rake release`, which will create a git tag for the version,
|
81
|
-
push git commits and tags, and push the `.gem` file to
|
82
|
-
[rubygems.org](https://rubygems.org).
|
83
159
|
|
84
160
|
## Contributing
|
85
161
|
|
data/Rakefile
CHANGED
data/cell.gemspec
CHANGED
@@ -0,0 +1,32 @@
|
|
1
|
+
module Cell
|
2
|
+
module ActiveJob
|
3
|
+
KEY = :'Cell.tenant'
|
4
|
+
|
5
|
+
def serialize
|
6
|
+
if (current_id = ::Cell::Model.current&.cell_id)
|
7
|
+
super.merge(KEY => current_id)
|
8
|
+
else
|
9
|
+
super
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def deserialize(job_data)
|
14
|
+
if job_data.key?(KEY)
|
15
|
+
self.cell_tenant = ::Cell::Model.cell_find(job_data[KEY])
|
16
|
+
end
|
17
|
+
super
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.prepended(cls)
|
21
|
+
cls.send(:attr_accessor, :cell_tenant)
|
22
|
+
cls.around_perform do |job, block|
|
23
|
+
::Cell::Model.use(job.cell_tenant, &block)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
|
30
|
+
if defined?(::ActiveJob)
|
31
|
+
::ActiveJob::Base.prepend(::Cell::ActiveJob)
|
32
|
+
end
|
data/lib/cell/clone_schema.rb
CHANGED
@@ -1,39 +1,41 @@
|
|
1
1
|
require 'active_record/schema_migration'
|
2
|
-
require 'cell/
|
2
|
+
require 'cell/meta'
|
3
3
|
|
4
4
|
module Cell
|
5
5
|
module CloneSchema
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
6
|
+
def self.connection
|
7
|
+
::ActiveRecord::Base.connection
|
8
|
+
end
|
9
|
+
|
10
|
+
|
11
|
+
def self.clone_schema(src, dst)
|
12
|
+
connection.execute <<~SQL
|
13
|
+
SELECT public.clone_schema(#{connection.quote(src)}, #{connection.quote(dst)}, TRUE)
|
10
14
|
SQL
|
11
|
-
con.execute(sql)
|
12
15
|
end
|
13
16
|
|
14
17
|
|
15
|
-
def copy_schema_migrations_to(dst
|
16
|
-
Cell::
|
17
|
-
connection: connection) do
|
18
|
+
def self.copy_schema_migrations_to(dst)
|
19
|
+
Cell::Meta.with_schema(dst, exclusive: true) do
|
18
20
|
::ActiveRecord::SchemaMigration.create_table
|
19
21
|
end
|
20
22
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
23
|
+
connection.execute <<~SQL
|
24
|
+
INSERT INTO #{connection.quote_schema_name(dst)}.schema_migrations
|
25
|
+
SELECT * from schema_migrations
|
26
|
+
SQL
|
25
27
|
end
|
26
28
|
|
27
29
|
|
28
|
-
def install_function!
|
30
|
+
def self.install_function!
|
29
31
|
@created ||= begin
|
30
|
-
|
32
|
+
connection.execute(function_definition)
|
31
33
|
true
|
32
34
|
end
|
33
35
|
end
|
34
36
|
|
35
37
|
|
36
|
-
def function_definition
|
38
|
+
def self.function_definition
|
37
39
|
File.read(__FILE__).split(/^__END__$/, 2)[-1]
|
38
40
|
end
|
39
41
|
end
|
data/lib/cell/console.rb
CHANGED
@@ -1,25 +1,19 @@
|
|
1
1
|
module Cell
|
2
2
|
module Console
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
if ENV['T']
|
10
|
-
Cell::Tenant.cell_find(ENV['T']).tap do |r|
|
11
|
-
fail "Couldn't locate tenant: #{ENV['T']}" if r.nil?
|
12
|
-
end
|
13
|
-
elsif Rails.env.development?
|
14
|
-
Cell::Tenant.first
|
3
|
+
def self.default_console_tenant
|
4
|
+
if ENV['T'].present?
|
5
|
+
Cell::Model.cell_find!(ENV['T'])
|
6
|
+
elsif Rails.env.development? && ENV['T'] != ''
|
7
|
+
Cell::Model.first
|
15
8
|
end
|
16
9
|
end
|
17
10
|
|
18
|
-
def configure!
|
11
|
+
def self.configure!
|
19
12
|
if (t = default_console_tenant)
|
20
|
-
::Cell::
|
13
|
+
::Cell::Model.set!(t)
|
21
14
|
end
|
22
15
|
end
|
23
|
-
|
24
16
|
end
|
25
17
|
end
|
18
|
+
|
19
|
+
::Cell::Console.configure!
|
data/lib/cell/context.rb
CHANGED
@@ -1,46 +1,65 @@
|
|
1
|
-
|
2
|
-
require '
|
1
|
+
require 'cell/meta'
|
2
|
+
require 'active_support/callbacks'
|
3
3
|
|
4
4
|
module Cell
|
5
5
|
module Context
|
6
6
|
THREAD_KEY = :'Cell.cell_id'
|
7
7
|
|
8
8
|
module ClassMethods
|
9
|
+
def self.extended(cls)
|
10
|
+
cls.include ActiveSupport::Callbacks
|
11
|
+
cls.define_callbacks :set_tenant, :use_tenant
|
12
|
+
end
|
13
|
+
|
14
|
+
def after_set(*args, &block)
|
15
|
+
set_callback(:set_tenant, :after, *args, &block)
|
16
|
+
end
|
17
|
+
|
18
|
+
def around_use(*args, &block)
|
19
|
+
set_callback(:use_tenant, :around, *args, &block)
|
20
|
+
end
|
21
|
+
|
9
22
|
def current
|
10
23
|
Thread.current[THREAD_KEY]
|
11
24
|
end
|
12
25
|
|
13
|
-
def
|
14
|
-
|
15
|
-
|
16
|
-
|
26
|
+
def set!(tenant, exclusive: false)
|
27
|
+
::Cell::Meta.with_schema(tenant.schema_name, exclusive: exclusive)
|
28
|
+
set_current(tenant)
|
29
|
+
tenant.run_callbacks :set_tenant
|
30
|
+
tenant
|
31
|
+
end
|
17
32
|
|
18
|
-
|
33
|
+
def use(tenant, exclusive: false)
|
34
|
+
fail ArgumentError, "block required" unless block_given?
|
35
|
+
saved_tenant = current
|
19
36
|
|
20
|
-
if
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
connection: connection) do
|
26
|
-
set_current(tenant)
|
27
|
-
yield Cell::Tenant.current
|
37
|
+
if tenant
|
38
|
+
::Cell::Meta.with_schema(tenant.schema_name, exclusive: exclusive) do
|
39
|
+
set_current(tenant)
|
40
|
+
tenant.run_callbacks :use_tenant do
|
41
|
+
yield Cell::Model.current
|
28
42
|
end
|
29
|
-
ensure
|
30
|
-
set_current(saved_tenant)
|
31
43
|
end
|
32
44
|
else
|
33
|
-
::Cell::
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
tenant
|
45
|
+
::Cell::Meta.with_global_schema do
|
46
|
+
set_current(nil)
|
47
|
+
yield nil
|
48
|
+
end
|
38
49
|
end
|
50
|
+
|
51
|
+
tenant
|
52
|
+
ensure
|
53
|
+
set_current(saved_tenant)
|
39
54
|
end
|
40
55
|
|
41
56
|
private
|
42
|
-
def set_current(
|
43
|
-
|
57
|
+
def set_current(tenant)
|
58
|
+
unless tenant.nil? || tenant.is_a?(Cell::Model)
|
59
|
+
fail ArgumentError, "Invalid tenant: #{tenant.inspect}"
|
60
|
+
end
|
61
|
+
|
62
|
+
Thread.current[THREAD_KEY] = tenant
|
44
63
|
end
|
45
64
|
end
|
46
65
|
|
@@ -48,14 +67,16 @@ module Cell
|
|
48
67
|
self.class.use(self, *args, &block)
|
49
68
|
end
|
50
69
|
|
70
|
+
def set!(tenant)
|
71
|
+
Cell::Model.set!(tenant)
|
72
|
+
end
|
73
|
+
|
51
74
|
def current?
|
52
75
|
self.class.current == self
|
53
76
|
end
|
54
77
|
|
55
78
|
def self.prepended(cls)
|
56
|
-
|
57
|
-
prepend ClassMethods
|
58
|
-
end
|
79
|
+
cls.extend(ClassMethods)
|
59
80
|
end
|
60
81
|
end
|
61
82
|
end
|
data/lib/cell/meta.rb
ADDED
@@ -0,0 +1,87 @@
|
|
1
|
+
require 'cell/schema'
|
2
|
+
require 'active_record/internal_metadata'
|
3
|
+
|
4
|
+
module Cell
|
5
|
+
module Meta
|
6
|
+
GLOBAL_KEY = 'Cell.global'
|
7
|
+
|
8
|
+
def self.connection
|
9
|
+
::ActiveRecord::Base.connection
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.with_schema(new_path, exclusive: false)
|
13
|
+
saved_path = connection.schema_search_path
|
14
|
+
active_path = exclusive ? new_path : "#{new_path},#{saved_path}"
|
15
|
+
|
16
|
+
set_schema_search_path(active_path)
|
17
|
+
|
18
|
+
if block_given?
|
19
|
+
begin
|
20
|
+
yield
|
21
|
+
ensure
|
22
|
+
set_schema_search_path(saved_path)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.set_schema_search_path(path)
|
28
|
+
if path != connection.schema_search_path
|
29
|
+
connection.schema_search_path = path
|
30
|
+
connection.clear_query_cache
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.global_schema
|
35
|
+
@global_schema ||= ActiveRecord::Base.connection.schema_search_path
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.prototype_schema
|
39
|
+
'cell_prototype'
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.structural_schema
|
43
|
+
"#{prototype_schema}, #{global_schema}"
|
44
|
+
end
|
45
|
+
|
46
|
+
def self.with_global_schema(&block)
|
47
|
+
with_schema(global_schema, exclusive: true, &block)
|
48
|
+
end
|
49
|
+
|
50
|
+
def self.with_prototype_schema(&block)
|
51
|
+
with_schema(prototype_schema, exclusive: true, &block)
|
52
|
+
end
|
53
|
+
|
54
|
+
def self.with_structural_schema(&block)
|
55
|
+
with_schema(structural_schema, exclusive: true, &block)
|
56
|
+
end
|
57
|
+
|
58
|
+
def self.global_tables
|
59
|
+
with_global_schema do
|
60
|
+
(::ActiveRecord::InternalMetadata[GLOBAL_KEY] || '').split(',')
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def self.set_global_tables(tables)
|
65
|
+
with_global_schema do
|
66
|
+
table_string = tables.map(&:to_s).sort.uniq.join(',')
|
67
|
+
::ActiveRecord::InternalMetadata[GLOBAL_KEY] = table_string
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def self.add_global_table(name)
|
72
|
+
set_global_tables(global_tables + [name.to_s])
|
73
|
+
end
|
74
|
+
|
75
|
+
def self.remove_global_table(name)
|
76
|
+
set_global_tables(global_tables - [name.to_s])
|
77
|
+
end
|
78
|
+
|
79
|
+
def self.global_model?(m)
|
80
|
+
@model_map ||= global_tables.map do |k|
|
81
|
+
[k, true]
|
82
|
+
end.to_h
|
83
|
+
|
84
|
+
@model_map[m.table_name] || false
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|