cell 0.1.1 → 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,21 +1,95 @@
1
1
  #
2
- # Alright, here's how this works:
2
+ # This is the messiest part of Cell, I think by necessity.
3
3
  #
4
- # A migration has two execution modes, global and targeted.
5
-
6
- # "global" is ran with a "db:migrate", which means no Tenant is activated.
7
- # "targeted" is ran once over each tenant, via "cell:db:migrate".
4
+ # A migration has two execution modes, global and targeted, with correspond to "db:migrate" and
5
+ # "cell:db:migrate"
6
+ #
7
+ # "global" is ran with a "db:migrate", which means no Tenant is activated. DDL commands here are
8
+ # directed to either 'public' or 'cell_prototype'
9
+ #
10
+ # "targeted" is ran with a tenant activated (cell:db:migrate), and all DDL commands are directed to
11
+ # the current tenant's schema.
8
12
  #
9
- # In global mode:
10
- # Each migration is ran twice: once with no explicit schema set, then again
11
- # with "tenant_prototype" activated.
13
+ # In 'rails db:migrate' (global mode), we actually execute each migration twice: once with a
14
+ # pass_context set to :global, where ONLY commands in a 'global {}' block are executed, and then
15
+ # with pass_context set to :prototype, where ONLY commands outside of a 'global {}' block are
16
+ # executed.
17
+ #
18
+ # ContextTracker keeps up with this.
12
19
 
20
+ require 'cell/meta'
13
21
  require 'cell/schema'
14
22
  require 'cell/clone_schema'
15
- require 'cell/with_schema'
23
+
16
24
 
17
25
  module Cell
18
26
  module Migration
27
+
28
+ # OK, ContextTracker keeps up with:
29
+ # * The context in which this migration is being ran, e.g. the :global first pass, the
30
+ # :prototype second pass, OR the :targeted per tenant pass.
31
+ # * If it's in a global block or not, which determines if actions are actually executed
32
+ # * If force_execution is set, which is for the benefit of playing back CommandRecorder
33
+ # commands.
34
+ module ContextTracker
35
+ mattr_accessor :pass_context
36
+ mattr_accessor :global_block
37
+ mattr_accessor :force_execution
38
+
39
+ def execute_ddl?
40
+ force_execution ||
41
+ (pass_context == :global && global_block) ||
42
+ (pass_context == :prototype && !global_block) ||
43
+ (pass_context == :target && !global_block)
44
+ end
45
+
46
+ # When the CommandRecorder's commands are executed, they're rewritten to go through
47
+ # force_call, because the "effective set of commands" have already been determined by the
48
+ # CommandRecorder run.
49
+ def force_call(*args, &block)
50
+ saved, self.force_execution = self.force_execution, true
51
+ send(*args, &block)
52
+ ensure
53
+ self.force_execution = saved
54
+ end
55
+
56
+ def with_context(context, search_path, exclusive: false)
57
+ Cell::Meta::with_schema(search_path, exclusive: exclusive) do
58
+ begin
59
+ saved, self.pass_context = self.pass_context, context
60
+ yield
61
+ ensure
62
+ self.pass_context = saved
63
+ end
64
+ end
65
+ end
66
+
67
+ def global
68
+ saved, self.global_block = self.global_block, true
69
+ yield
70
+ ensure
71
+ self.global_block = saved
72
+ end
73
+ end
74
+
75
+
76
+ # This module intercepts create_table and drop_table, and updates the list of global tables
77
+ # in ::ActiveRecord::InternalMetadata['cell.global'].
78
+ module MetadataIntercept
79
+ def create_table(name, *args, &block)
80
+ super.tap do
81
+ ::Cell::Meta.add_global_table(name) if pass_context == :global
82
+ end
83
+ end
84
+
85
+ def drop_table(name, *args, &block)
86
+ super.tap do
87
+ ::Cell::Meta.remove_global_table(name) if pass_context == :global
88
+ end
89
+ end
90
+ end
91
+
92
+ # We intercept every method given, and only execute it if appropriate according to execute_ddl?
19
93
  def self.intercept(methods)
20
94
  methods.each do |method|
21
95
  define_method(method) do |*args, &block|
@@ -24,7 +98,8 @@ module Cell
24
98
  end
25
99
  end
26
100
 
27
- # This sucks.
101
+ # This sucks. In the future, we may want to pull these from
102
+ # SchemaStatements
28
103
  intercept %i(add_belongs_to add_column add_foreign_key add_index
29
104
  add_index_sort_order add_reference add_timestamps
30
105
  change_column change_column_default change_column_null
@@ -38,21 +113,12 @@ module Cell
38
113
  %i(enable_extension disable_extension truncate) +
39
114
  %i(execute)
40
115
 
41
- attr_accessor :pass_context
42
- attr_accessor :global_block
43
-
44
- def execute_ddl?
45
- (pass_context == :global && global_block) ||
46
- (pass_context == :prototype && !global_block) ||
47
- (pass_context == :target && !global_block)
48
- end
49
-
50
116
  def global_schema
51
- Cell::Schema.global_schema
117
+ Cell::Meta.global_schema
52
118
  end
53
119
 
54
120
  def prototype_schema
55
- Cell::Schema.prototype_schema
121
+ Cell::Meta.prototype_schema
56
122
  end
57
123
 
58
124
  def tenant_schema
@@ -60,28 +126,16 @@ module Cell
60
126
  end
61
127
 
62
128
  def target
63
- Cell::Tenant.current
129
+ Cell::Model.current
64
130
  end
65
131
 
66
132
  def targeted?
67
133
  !! target
68
134
  end
69
135
 
70
- def with_context(context, search_path)
71
- Cell::WithSchema::with_schema(search_path, connection: connection) do
72
- begin
73
- saved_context = self.pass_context
74
- self.pass_context = context
75
- yield
76
- ensure
77
- self.pass_context = saved_context
78
- end
79
- end
80
- end
81
-
82
-
136
+ # This is our super-special initialization function.
83
137
  def initialize_cell!
84
- CloneSchema.install_function!(connection)
138
+ CloneSchema.install_function!
85
139
  execute "CREATE SCHEMA #{connection.quote_schema_name(prototype_schema)}"
86
140
  end
87
141
 
@@ -95,20 +149,53 @@ module Cell
95
149
  super
96
150
  end
97
151
  else
98
- with_context(:target, tenant_schema) do
152
+ with_context(:target, tenant_schema, exclusive: true) do
99
153
  super
100
154
  end
101
155
  end
102
156
  end
103
157
 
104
- def global
105
- saved = self.global_block
106
- self.global_block = true
107
- yield
108
- ensure
109
- self.global_block = saved
158
+ ActiveSupport.on_load(:active_record) do
159
+ ActiveRecord::Migration.prepend(::Cell::Migration::MetadataIntercept)
160
+ ActiveRecord::Migration.prepend(::Cell::Migration::ContextTracker)
161
+ ActiveRecord::Migration.prepend(::Cell::Migration)
110
162
  end
111
163
 
112
- ActiveRecord::Migration.prepend(self)
164
+
165
+ # Patches to CommandRecorder, which let us roll back.
166
+ module CommandRecorderFilter
167
+ # This maybe should've been attr_reader in CommandRecorder
168
+ def commands=(*)
169
+ # If this is actually used, we're fucked.
170
+ fail "The problem with monkey patching is..."
171
+ end
172
+
173
+ def commands
174
+ @commands.select do |command|
175
+ command[0]
176
+ end.map do |command|
177
+ [:force_call, [command[1], *command[2]], command[3]]
178
+ end
179
+ end
180
+
181
+ # saves the state of a recorded command with the context it was in.
182
+ def add_command(command)
183
+ @commands << [execute_ddl?, *command]
184
+ end
185
+
186
+ # We override #record to proxy through add_command
187
+ def record(*command, &block)
188
+ if @reverting
189
+ add_command inverse_of(*command, &block)
190
+ else
191
+ add_command (command << block)
192
+ end
193
+ end
194
+
195
+ ActiveSupport.on_load(:active_record) do
196
+ ::ActiveRecord::Migration::CommandRecorder.prepend(::Cell::Migration::ContextTracker)
197
+ ::ActiveRecord::Migration::CommandRecorder.prepend(::Cell::Migration::CommandRecorderFilter)
198
+ end
199
+ end
113
200
  end
114
201
  end
@@ -0,0 +1,85 @@
1
+ # These are added to ActiveRecord::Base, regardless if they're the tenant model or not.
2
+
3
+ # There are two big tricks here:
4
+ # 1. Assigning a "tenant" attribute to every model that is loaded from the db, or created while a
5
+ # tenant is active.
6
+ # 2. Making sure the object is validated and saved with the proper tenant activated, even if the
7
+ # tenant has changed since the object was created
8
+ #
9
+ # So you can load an object from "tenant_a", switch to "tenant_b", and if you save or validate the
10
+ # object, it'll do so while switched to "tenant_a", and then restore.
11
+
12
+ require 'cell/meta'
13
+
14
+ module Cell
15
+ module ModelExtensions
16
+
17
+ # Say we have a model 'u', that is from an old Tenant, loading an association like 'u.books'
18
+ # should swap into u.tenant while fetching.
19
+ module AssociationExtensions
20
+ def load_target
21
+ owner._activate_tenant do
22
+ super
23
+ end
24
+ end
25
+ end
26
+
27
+
28
+ module ClassMethods
29
+ def global_model?
30
+ ::Cell::Meta.global_model?(self)
31
+ end
32
+
33
+ # When a model's schema is lazy-loaded, we want to make sure our view of the DB allows us to
34
+ # get introspection data.
35
+ def load_schema!
36
+ ::Cell::Meta.with_structural_schema do
37
+ return super
38
+ end
39
+ end
40
+ end
41
+
42
+ attr_reader :tenant
43
+
44
+ def _assign_tenant
45
+ unless self.class.global_model?
46
+ @tenant = Cell::Model.current
47
+ end
48
+ end
49
+
50
+ def _activate_tenant(&block)
51
+ if tenant && tenant != ::Cell::Model.current
52
+ tenant.use(&block)
53
+ else
54
+ yield
55
+ end
56
+ end
57
+
58
+ # undocumented, but makes sure validations are ran in the context of the object's tenant, e.g.,
59
+ # uniqueness.
60
+ def run_validations!
61
+ _activate_tenant do
62
+ super
63
+ end
64
+ end
65
+
66
+ def self.prepended(cls)
67
+ class << cls
68
+ prepend ClassMethods
69
+ end
70
+
71
+ cls.after_initialize :_assign_tenant
72
+ cls.around_save :_activate_tenant
73
+ end
74
+
75
+ def self.install!
76
+ ActiveSupport.on_load(:active_record) do
77
+ ::ActiveRecord::Base.prepend(::Cell::ModelExtensions)
78
+ ::ActiveRecord::Associations::Association.prepend(::Cell::ModelExtensions::AssociationExtensions)
79
+ ::ActiveRecord::Associations::CollectionAssociation.prepend(::Cell::ModelExtensions::AssociationExtensions)
80
+ end
81
+ end
82
+ end
83
+ end
84
+
85
+ Cell::ModelExtensions.install!
data/lib/cell/railtie.rb CHANGED
@@ -6,12 +6,26 @@ module Cell
6
6
 
7
7
  console do
8
8
  require 'cell/console'
9
- Cell::Console.configure!
10
9
  end
11
10
 
12
- initializer "cell.sanity_check" do
11
+ config.after_initialize do
12
+ require 'cell/tenant'
13
13
  require 'cell/sanity_check'
14
- Cell::SanityCheck.sanity_check!
14
+ require 'cell/model_extensions'
15
+ require 'cell/migration'
16
+ require 'cell/active_job'
15
17
  end
16
18
  end
19
+
20
+
21
+ def self.const_missing(name)
22
+ return super unless name == :Model
23
+
24
+ Rails.application.eager_load!
25
+ unless const_defined?(:Model)
26
+ fail "Eager loaded models to find model, didn't pan out." +
27
+ "Make sure one of your models has `prepend Cell::Tenant`"
28
+ end
29
+ ::Cell::Model
30
+ end
17
31
  end
@@ -2,19 +2,16 @@ require 'active_record/connection_adapters/postgresql_adapter'
2
2
 
3
3
  module Cell
4
4
  module SanityCheck
5
-
6
- module_function
7
- def check_active_record_adapter!
5
+ def self.check_active_record_adapter!
8
6
  pg_base_adapter = ::ActiveRecord::ConnectionAdapters::PostgreSQLAdapter
9
- whitelist = ["PostgreSQLAdapter"]
7
+ whitelist = []
10
8
  adapter_name = ActiveRecord::Base.connection.adapter_name
11
9
 
12
10
  unless ActiveRecord::Base.connection.is_a?(pg_base_adapter) ||
13
11
  whitelist.include?(adapter_name)
14
12
  msg <<~EOD
15
- Cell uses PostgreSQL-specific features that cannot be represented
16
- with your adapter. If your adapter is a PostgreSQL spin-off, please
17
- open a pull request.
13
+ Cell uses PostgreSQL-specific features that cannot be represented with your adapter. If
14
+ your adapter is a PostgreSQL spin-off, please open a pull request.
18
15
 
19
16
  Whitelist: #{whitelist.inspect}
20
17
  ActiveRecord adapter: #{adapter_name}
@@ -24,13 +21,12 @@ module Cell
24
21
  end
25
22
 
26
23
 
27
- def check_schema_format!
24
+ def self.check_schema_format!
28
25
  if Rails.application.config.active_record.schema_format != :sql
29
26
  msg = <<~EOD
30
- Cell uses PostgreSQL-specific features that cannot be represented
31
- using a schema_format other than :sql. You need a definititive
32
- structure.sql instead of a schema.rb. You can configure this by
33
- adding the following line to your application.rb:
27
+ Cell uses PostgreSQL-specific features that cannot be represented using a schema_format
28
+ other than :sql. You need a definititive structure.sql instead of a schema.rb. You can
29
+ configure this by adding the following line to your application.rb:
34
30
 
35
31
  Rails.application.config.active_record.schema_format = :sql
36
32
  EOD
@@ -38,18 +34,16 @@ module Cell
38
34
  end
39
35
  end
40
36
 
41
- def check_dump_schemas!
37
+ def self.check_dump_schemas!
42
38
  dump_schemas = Rails.application.config.active_record.dump_schemas
43
39
  unless dump_schemas.to_s.match(/\bcell_prototype\b/)
44
40
  msg = <<~EOD
45
- Cell stores tenant templates in a PostgreSQL schema called
46
- "cell_prototype".
41
+ Cell stores tenant templates in a PostgreSQL schema called "cell_prototype".
47
42
 
48
- Rails will not dump this schema by default with `db:structure:dump`
49
- without explicitly setting `dump_schemas`.
43
+ Rails will not dump this schema by default with `db:structure:dump` without explicitly
44
+ setting `dump_schemas`.
50
45
 
51
- You can configure this properly by adding a line like the following
52
- in application.rb:
46
+ You can configure this properly by adding a line like the following in application.rb:
53
47
 
54
48
  Rails.application.config.active_record.dump_schemas = "public,cell_prototype"
55
49
  EOD
@@ -57,10 +51,14 @@ module Cell
57
51
  end
58
52
  end
59
53
 
60
- def sanity_check!
61
- check_active_record_adapter!
62
- check_schema_format!
63
- check_dump_schemas!
54
+ def self.sanity_check!
55
+ ActiveSupport.on_load(:active_record) do
56
+ Cell::SanityCheck.check_active_record_adapter!
57
+ Cell::SanityCheck.check_schema_format!
58
+ Cell::SanityCheck.check_dump_schemas!
59
+ end
64
60
  end
65
61
  end
66
62
  end
63
+
64
+ ::Cell::SanityCheck.sanity_check!
data/lib/cell/schema.rb CHANGED
@@ -8,16 +8,6 @@ module Cell
8
8
  MAX_SCHEMA_NAME_LENGTH = 63 # PostgreSQL baked-in default
9
9
  MAX_CELL_ID_SIZE = MAX_SCHEMA_NAME_LENGTH - SCHEMA_PREFIX.size
10
10
 
11
- module_function \
12
- def global_schema
13
- @global_schema ||= ActiveRecord::Base.connection.schema_search_path
14
- end
15
-
16
- module_function \
17
- def prototype_schema
18
- 'cell_prototype'
19
- end
20
-
21
11
  module ClassMethods
22
12
  def schema_name_for_cell_id(cell_id)
23
13
  SCHEMA_PREFIX + cell_id.to_s.gsub(/[^a-z0-9_]/i, '-')
@@ -32,9 +22,8 @@ module Cell
32
22
  def create_schema!
33
23
  con = self.class.connection
34
24
  con.transaction do
35
- Cell::CloneSchema.clone_schema(con, Cell::Schema.prototype_schema,
36
- schema_name)
37
- Cell::CloneSchema.copy_schema_migrations_to(schema_name, connection: con)
25
+ Cell::CloneSchema.clone_schema(Cell::Meta.prototype_schema, schema_name)
26
+ Cell::CloneSchema.copy_schema_migrations_to(schema_name)
38
27
  end
39
28
  end
40
29
 
@@ -9,9 +9,8 @@ def wrappable_task?(task)
9
9
  fail ArgumentError unless task.name.start_with?('db:')
10
10
 
11
11
  case task.name
12
- when /^db:test\b/, /^db:structure\b/, /^db:schema\b/, /^db:setup\b/,
13
- /^db:purge\b/, /^db:reset\b/, /^db:load_config\b/, /^db:create\b/,
14
- /^db:drop\b/
12
+ when /^db:test\b/, /^db:structure\b/, /^db:schema\b/, /^db:setup\b/, /^db:purge\b/,
13
+ /^db:reset\b/, /^db:load_config\b/, /^db:create\b/, /^db:drop\b/
15
14
  false
16
15
  else
17
16
  true
@@ -20,32 +19,40 @@ end
20
19
 
21
20
 
22
21
  def using_each_tenant(&block)
23
- # We have to get the model that included Cell::Tenant
24
- require Rails.root.join('config/environment')
25
-
26
22
  tenants = if ENV['T']
27
23
  ENV['T'].split(/\s*,\s*/).map do |tenant_id|
28
- Cell::Tenant.cell_find(tenant_id).tap do |r|
24
+ ::Cell::Model.cell_find(tenant_id).tap do |r|
29
25
  fail "Unable to load tenant: #{tenant_id}" if r.nil?
30
26
  end
31
27
  end
32
28
  else
33
- ::Cell::Tenant.respond_to?(:find_each) ? ::Cell::Tenant.find_each
34
- : ::Cell::Tenant.each
29
+ ::Cell::Model.respond_to?(:find_each) ? ::Cell::Model.find_each : ::Cell::Model.each
35
30
  end
36
31
 
37
32
  tenants.each do |t|
38
- Cell::Tenant.use(t, &block)
33
+ Cell::Model.use(t, &block)
34
+ end
35
+ end
36
+
37
+ namespace :cell do
38
+ task :eager => 'environment' do
39
+ Rails.application.eager_load!
39
40
  end
40
41
  end
41
42
 
42
43
 
43
44
  db_tasks.each do |db_task|
44
45
  if wrappable_task?(db_task)
46
+ # We need to have the model the user has chosen to be Cell::Model to have
47
+ # been loaded. The easy way is to punt and eager_load all rake tasks, but
48
+ # we'll try to selectively do it for certain tasks by adding the
49
+ # dependency.
50
+ task db_task => 'cell:eager'
51
+
45
52
  if db_task.comment
46
53
  desc "run `#{db_task.name}' for all tenants, or each in $T=t1,t2,..."
47
54
  end
48
- task "cell:#{db_task.name}" => ['environment', 'db:load_config'] do |t|
55
+ task "cell:#{db_task.name}" => 'cell:eager' do |t|
49
56
  Rails.application.eager_load!
50
57
  using_each_tenant do |tenant|
51
58
  puts "Running #{db_task.name} for tenant '#{tenant.cell_id}'"
@@ -0,0 +1,55 @@
1
+ require 'cell/context'
2
+ require 'cell/schema'
3
+ require 'cell/url_options'
4
+
5
+ module Cell
6
+ module Tenant
7
+ class << self
8
+ attr_accessor :cls
9
+ end
10
+
11
+ module ClassMethods
12
+ def cell_id_column
13
+ :id
14
+ end
15
+
16
+ def cell_find(cell_id, finder_method: :find_by)
17
+ send(finder_method, cell_id_column => cell_id)
18
+ end
19
+
20
+ def cell_find!(cell_id)
21
+ cell_find(cell_id, finder_method: :find_by!)
22
+ end
23
+ end
24
+
25
+ def cell_id
26
+ send(self.class.cell_id_column)
27
+ end
28
+
29
+ def cell_id_changed?
30
+ !! previous_changes[self.class.cell_id_column]
31
+ end
32
+
33
+ def cell_id_change_set
34
+ fail "cell_id was not changed" unless cell_id_changed?
35
+ previous_changes[self.class.cell_id_column]
36
+ end
37
+
38
+ def self.included(model)
39
+ fail "Use `prepend` instead of `include`."
40
+ end
41
+
42
+ def self.prepended(model)
43
+ model.extend(ClassMethods)
44
+
45
+ if ::Cell.const_defined?(:Model)
46
+ ::Cell.send(:remove_const, :Model)
47
+ end
48
+ ::Cell.const_set(:Model, model)
49
+
50
+ model.prepend(::Cell::Schema)
51
+ model.prepend(::Cell::Context)
52
+ model.prepend(::Cell::UrlOptions)
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,26 @@
1
+ module Cell
2
+ module UrlOptions
3
+ def self.prepended(cls)
4
+ cls.after_set :change_url_options, if: :has_cell_host?
5
+ cls.around_use :change_url_options, if: :has_cell_host?
6
+ end
7
+
8
+ def has_cell_host?
9
+ respond_to?(:cell_host)
10
+ end
11
+
12
+ private
13
+ def change_url_options(&block)
14
+ saved = Rails.application.routes.default_url_options[:host]
15
+ Rails.application.routes.default_url_options[:host] = cell_host
16
+
17
+ if block_given?
18
+ begin
19
+ yield
20
+ ensure
21
+ Rails.application.routes.default_url_options[:host] = saved
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
data/lib/cell/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Cell
2
- VERSION = '0.1.1'
2
+ VERSION = '0.1.2'
3
3
  end
data/lib/cell.rb CHANGED
@@ -1,7 +1,2 @@
1
1
  require 'cell/version'
2
- require 'cell/configure'
3
-
4
- if defined?(Rails)
5
- require 'cell/migration'
6
- require 'cell/railtie'
7
- end
2
+ require 'cell/railtie' if defined?(Rails)
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cell
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mike Owens
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2016-10-08 00:00:00.000000000 Z
11
+ date: 2016-11-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -78,14 +78,14 @@ dependencies:
78
78
  requirements:
79
79
  - - "~>"
80
80
  - !ruby/object:Gem::Version
81
- version: '10.0'
81
+ version: '11.0'
82
82
  type: :development
83
83
  prerelease: false
84
84
  version_requirements: !ruby/object:Gem::Requirement
85
85
  requirements:
86
86
  - - "~>"
87
87
  - !ruby/object:Gem::Version
88
- version: '10.0'
88
+ version: '11.0'
89
89
  description:
90
90
  email:
91
91
  - mike@meter.md
@@ -103,22 +103,20 @@ files:
103
103
  - bin/test
104
104
  - cell.gemspec
105
105
  - lib/cell.rb
106
+ - lib/cell/active_job.rb
106
107
  - lib/cell/clone_schema.rb
107
- - lib/cell/configure.rb
108
108
  - lib/cell/console.rb
109
109
  - lib/cell/context.rb
110
- - lib/cell/data_directory.rb
110
+ - lib/cell/meta.rb
111
111
  - lib/cell/migration.rb
112
- - lib/cell/model.rb
112
+ - lib/cell/model_extensions.rb
113
113
  - lib/cell/railtie.rb
114
114
  - lib/cell/sanity_check.rb
115
115
  - lib/cell/schema.rb
116
- - lib/cell/sidekiq.rb
117
116
  - lib/cell/tasks/cell.rake
117
+ - lib/cell/tenant.rb
118
+ - lib/cell/url_options.rb
118
119
  - lib/cell/version.rb
119
- - lib/cell/with_schema.rb
120
- - lib/dump/tenant_routing_manager.rb
121
- - lib/dump/tenant_url_options.rb
122
120
  homepage: https://github.com/metermd/cell
123
121
  licenses:
124
122
  - MIT