cell 0.1.2 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,198 @@
1
+ #
2
+ # This is the messiest part of Cell, I think by necessity.
3
+ #
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.
12
+ #
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.
19
+
20
+ require 'cell/meta'
21
+ require 'cell/schema'
22
+ require 'cell/clone_schema'
23
+
24
+
25
+ module Cell
26
+ module Ext
27
+ module Migration
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
+ 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
+ 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
+ Meta.remove_global_table(name) if pass_context == :global
88
+ end
89
+ end
90
+ end
91
+
92
+ # We intercept these methods, and only execute if appropriate according to execute_ddl?
93
+ def self.intercept(methods)
94
+ methods.each do |method|
95
+ define_method(method) do |*args, &block|
96
+ super(*args, &block) if execute_ddl?
97
+ end
98
+ end
99
+ end
100
+
101
+ # This sucks. In the future, we may want to pull these from SchemaStatements
102
+ intercept %i(add_belongs_to add_column add_foreign_key add_index
103
+ add_index_sort_order add_reference add_timestamps
104
+ change_column change_column_default change_column_null
105
+ change_table change_table_comment create_join_table
106
+ create_table drop_join_table drop_table
107
+ initialize_schema_migrations_table remove_belongs_to
108
+ remove_column remove_columns remove_foreign_key remove_index
109
+ remove_reference remove_timestamps rename_column
110
+ rename_column_indexes rename_index rename_table
111
+ rename_table_indexes table_alias_for table_comment) +
112
+ %i(enable_extension disable_extension truncate) +
113
+ %i(execute)
114
+
115
+ def global_schema
116
+ Meta.global_schema
117
+ end
118
+
119
+ def prototype_schema
120
+ Meta.prototype_schema
121
+ end
122
+
123
+ def tenant_schema
124
+ target.schema_name
125
+ end
126
+
127
+ def target
128
+ Model.current
129
+ end
130
+
131
+ def targeted?
132
+ !! target
133
+ end
134
+
135
+ # This is our super-special initialization function.
136
+ def initialize_cell!
137
+ CloneSchema.install_function!
138
+ execute "CREATE SCHEMA #{connection.quote_schema_name(prototype_schema)}"
139
+ end
140
+
141
+ def exec_migration(con, direction)
142
+ if ! targeted?
143
+ with_context(:global, global_schema) do
144
+ super
145
+ end
146
+
147
+ with_context(:prototype, prototype_schema) do
148
+ super
149
+ end
150
+ else
151
+ with_context(:target, tenant_schema, exclusive: true) do
152
+ super
153
+ end
154
+ end
155
+ end
156
+
157
+ # Patches to CommandRecorder, which let us roll back.
158
+ module CommandRecorderFilter
159
+ # This maybe should've been attr_reader in CommandRecorder
160
+ def commands=(*)
161
+ # If this is actually used, we're fucked.
162
+ fail "The problem with monkey patching is..."
163
+ end
164
+
165
+ def commands
166
+ @commands.select do |command|
167
+ command[0]
168
+ end.map do |command|
169
+ [:force_call, [command[1], *command[2]], command[3]]
170
+ end
171
+ end
172
+
173
+ # saves the state of a recorded command with the context it was in.
174
+ def add_command(command)
175
+ @commands << [execute_ddl?, *command]
176
+ end
177
+
178
+ # We override #record to proxy through add_command
179
+ def record(*command, &block)
180
+ if @reverting
181
+ add_command inverse_of(*command, &block)
182
+ else
183
+ add_command (command << block)
184
+ end
185
+ end
186
+ end
187
+ end
188
+ end
189
+ end
190
+
191
+ ActiveSupport.on_load(:active_record) do
192
+ ActiveRecord::Migration.prepend(Cell::Ext::Migration::ContextTracker)
193
+ ActiveRecord::Migration.prepend(Cell::Ext::Migration::MetadataIntercept)
194
+ ActiveRecord::Migration.prepend(Cell::Ext::Migration)
195
+
196
+ ActiveRecord::Migration::CommandRecorder.prepend(Cell::Ext::Migration::ContextTracker)
197
+ ActiveRecord::Migration::CommandRecorder.prepend(Cell::Ext::Migration::CommandRecorderFilter)
198
+ end
data/lib/cell/meta.rb CHANGED
@@ -32,7 +32,8 @@ module Cell
32
32
  end
33
33
 
34
34
  def self.global_schema
35
- @global_schema ||= ActiveRecord::Base.connection.schema_search_path
35
+ # This could be racy?
36
+ @global_schema ||= ::ActiveRecord::Base.connection.schema_search_path
36
37
  end
37
38
 
38
39
  def self.prototype_schema
@@ -40,7 +41,7 @@ module Cell
40
41
  end
41
42
 
42
43
  def self.structural_schema
43
- "#{prototype_schema}, #{global_schema}"
44
+ "#{prototype_schema},#{global_schema}"
44
45
  end
45
46
 
46
47
  def self.with_global_schema(&block)
data/lib/cell/railtie.rb CHANGED
@@ -11,21 +11,20 @@ module Cell
11
11
  config.after_initialize do
12
12
  require 'cell/tenant'
13
13
  require 'cell/sanity_check'
14
- require 'cell/model_extensions'
15
- require 'cell/migration'
16
- require 'cell/active_job'
14
+ require 'cell/ext/active_record'
15
+ require 'cell/ext/migration'
16
+ require 'cell/ext/active_job'
17
17
  end
18
18
  end
19
19
 
20
-
21
20
  def self.const_missing(name)
22
21
  return super unless name == :Model
23
22
 
24
23
  Rails.application.eager_load!
25
24
  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`"
25
+ fail "Eager loaded models to find one that uses `include Cell::Tenant`. Didn't pan out."
28
26
  end
29
- ::Cell::Model
27
+
28
+ Model
30
29
  end
31
30
  end
@@ -3,7 +3,7 @@ require 'active_record/connection_adapters/postgresql_adapter'
3
3
  module Cell
4
4
  module SanityCheck
5
5
  def self.check_active_record_adapter!
6
- pg_base_adapter = ::ActiveRecord::ConnectionAdapters::PostgreSQLAdapter
6
+ pg_base_adapter = ActiveRecord::ConnectionAdapters::PostgreSQLAdapter
7
7
  whitelist = []
8
8
  adapter_name = ActiveRecord::Base.connection.adapter_name
9
9
 
@@ -18,6 +18,8 @@ module Cell
18
18
  EOD
19
19
  fail msg
20
20
  end
21
+ rescue ActiveRecord::NoDatabaseError
22
+ # Not our problem
21
23
  end
22
24
 
23
25
 
@@ -43,7 +45,7 @@ module Cell
43
45
  Rails will not dump this schema by default with `db:structure:dump` without explicitly
44
46
  setting `dump_schemas`.
45
47
 
46
- You can configure this properly by adding a line like the following in application.rb:
48
+ You can configure this by adding a line like the following in application.rb:
47
49
 
48
50
  Rails.application.config.active_record.dump_schemas = "public,cell_prototype"
49
51
  EOD
@@ -51,14 +53,11 @@ module Cell
51
53
  end
52
54
  end
53
55
 
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
60
- end
61
56
  end
62
57
  end
63
58
 
64
- ::Cell::SanityCheck.sanity_check!
59
+ ActiveSupport.on_load(:active_record) do
60
+ Cell::SanityCheck.check_active_record_adapter!
61
+ Cell::SanityCheck.check_schema_format!
62
+ Cell::SanityCheck.check_dump_schemas!
63
+ end
data/lib/cell/schema.rb CHANGED
@@ -8,29 +8,26 @@ 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 ClassMethods
12
- def schema_name_for_cell_id(cell_id)
13
- SCHEMA_PREFIX + cell_id.to_s.gsub(/[^a-z0-9_]/i, '-')
14
- end
11
+ def self.schema_name_for_cell_id(cell_id)
12
+ SCHEMA_PREFIX + cell_id.to_s.gsub(/[^a-z0-9_]/i, '-')
15
13
  end
16
14
 
17
15
  def schema_name
18
- self.class.schema_name_for_cell_id(cell_id)
16
+ Schema.schema_name_for_cell_id(cell_id)
19
17
  end
20
18
 
21
19
  private
22
20
  def create_schema!
23
21
  con = self.class.connection
24
22
  con.transaction do
25
- Cell::CloneSchema.clone_schema(Cell::Meta.prototype_schema, schema_name)
26
- Cell::CloneSchema.copy_schema_migrations_to(schema_name)
23
+ CloneSchema.clone_schema(Meta.prototype_schema, schema_name)
24
+ CloneSchema.copy_schema_migrations_to(schema_name)
27
25
  end
28
26
  end
29
27
 
30
28
  def destroy_schema!
31
29
  con = self.class.connection
32
- con.execute \
33
- "DROP SCHEMA #{con.quote_schema_name(schema_name)} CASCADE"
30
+ con.execute "DROP SCHEMA #{con.quote_schema_name(schema_name)} CASCADE"
34
31
  end
35
32
 
36
33
  def update_schema!
@@ -38,13 +35,11 @@ module Cell
38
35
  src = self.class.schema_name_for_cell_id(src)
39
36
 
40
37
  con = self.class.connection
41
- con.execute \
42
- "ALTER SCHEMA #{con.quote_schema_name(src)} RENAME " +
43
- "TO #{con.quote_schema_name(schema_name)}"
38
+ con.execute "ALTER SCHEMA #{con.quote_schema_name(src)} RENAME " +
39
+ "TO #{con.quote_schema_name(schema_name)}"
44
40
  end
45
41
 
46
42
  def self.prepended(cls)
47
- cls.extend(ClassMethods)
48
43
  cls.after_create_commit :create_schema!
49
44
  cls.after_update_commit :update_schema!, if: :cell_id_changed?
50
45
  cls.after_destroy_commit :destroy_schema!
data/lib/cell/tenant.rb CHANGED
@@ -4,10 +4,6 @@ require 'cell/url_options'
4
4
 
5
5
  module Cell
6
6
  module Tenant
7
- class << self
8
- attr_accessor :cls
9
- end
10
-
11
7
  module ClassMethods
12
8
  def cell_id_column
13
9
  :id
@@ -35,21 +31,26 @@ module Cell
35
31
  previous_changes[self.class.cell_id_column]
36
32
  end
37
33
 
38
- def self.included(model)
39
- fail "Use `prepend` instead of `include`."
34
+ def self.append_features(cls)
35
+ cls.prepend(self)
40
36
  end
41
37
 
42
- def self.prepended(model)
43
- model.extend(ClassMethods)
38
+ def self.extend_object(cls)
39
+ cls.prepend(self)
40
+ end
44
41
 
45
- if ::Cell.const_defined?(:Model)
46
- ::Cell.send(:remove_const, :Model)
47
- end
48
- ::Cell.const_set(:Model, model)
42
+ def self.prepended(model)
43
+ Cell.assign_model(model)
49
44
 
45
+ model.extend(ClassMethods)
50
46
  model.prepend(::Cell::Schema)
51
47
  model.prepend(::Cell::Context)
52
48
  model.prepend(::Cell::UrlOptions)
53
49
  end
54
50
  end
51
+
52
+ def self.assign_model(model)
53
+ remove_const(:Model) if const_defined?(:Model)
54
+ const_set(:Model, model)
55
+ end
55
56
  end
data/lib/cell/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Cell
2
- VERSION = '0.1.2'
2
+ VERSION = '0.2.0'
3
3
  end
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.2
4
+ version: 0.2.0
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-11-05 00:00:00.000000000 Z
11
+ date: 2018-11-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -16,76 +16,70 @@ dependencies:
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: 5.0.0
20
- - - ">="
21
- - !ruby/object:Gem::Version
22
- version: 5.0.0.1
19
+ version: 5.2.0
23
20
  type: :runtime
24
21
  prerelease: false
25
22
  version_requirements: !ruby/object:Gem::Requirement
26
23
  requirements:
27
24
  - - "~>"
28
25
  - !ruby/object:Gem::Version
29
- version: 5.0.0
30
- - - ">="
31
- - !ruby/object:Gem::Version
32
- version: 5.0.0.1
26
+ version: 5.2.0
33
27
  - !ruby/object:Gem::Dependency
34
28
  name: pg
35
29
  requirement: !ruby/object:Gem::Requirement
36
30
  requirements:
37
31
  - - "~>"
38
32
  - !ruby/object:Gem::Version
39
- version: 0.18.4
33
+ version: 1.1.3
40
34
  type: :runtime
41
35
  prerelease: false
42
36
  version_requirements: !ruby/object:Gem::Requirement
43
37
  requirements:
44
38
  - - "~>"
45
39
  - !ruby/object:Gem::Version
46
- version: 0.18.4
40
+ version: 1.1.3
47
41
  - !ruby/object:Gem::Dependency
48
42
  name: minitest
49
43
  requirement: !ruby/object:Gem::Requirement
50
44
  requirements:
51
45
  - - "~>"
52
46
  - !ruby/object:Gem::Version
53
- version: 5.9.0
47
+ version: '5.11'
54
48
  type: :development
55
49
  prerelease: false
56
50
  version_requirements: !ruby/object:Gem::Requirement
57
51
  requirements:
58
52
  - - "~>"
59
53
  - !ruby/object:Gem::Version
60
- version: 5.9.0
54
+ version: '5.11'
61
55
  - !ruby/object:Gem::Dependency
62
56
  name: bundler
63
57
  requirement: !ruby/object:Gem::Requirement
64
58
  requirements:
65
59
  - - "~>"
66
60
  - !ruby/object:Gem::Version
67
- version: '1.12'
61
+ version: '1.16'
68
62
  type: :development
69
63
  prerelease: false
70
64
  version_requirements: !ruby/object:Gem::Requirement
71
65
  requirements:
72
66
  - - "~>"
73
67
  - !ruby/object:Gem::Version
74
- version: '1.12'
68
+ version: '1.16'
75
69
  - !ruby/object:Gem::Dependency
76
70
  name: rake
77
71
  requirement: !ruby/object:Gem::Requirement
78
72
  requirements:
79
73
  - - "~>"
80
74
  - !ruby/object:Gem::Version
81
- version: '11.0'
75
+ version: '12.3'
82
76
  type: :development
83
77
  prerelease: false
84
78
  version_requirements: !ruby/object:Gem::Requirement
85
79
  requirements:
86
80
  - - "~>"
87
81
  - !ruby/object:Gem::Version
88
- version: '11.0'
82
+ version: '12.3'
89
83
  description:
90
84
  email:
91
85
  - mike@meter.md
@@ -103,13 +97,13 @@ files:
103
97
  - bin/test
104
98
  - cell.gemspec
105
99
  - lib/cell.rb
106
- - lib/cell/active_job.rb
107
100
  - lib/cell/clone_schema.rb
108
101
  - lib/cell/console.rb
109
102
  - lib/cell/context.rb
103
+ - lib/cell/ext/active_job.rb
104
+ - lib/cell/ext/active_record.rb
105
+ - lib/cell/ext/migration.rb
110
106
  - lib/cell/meta.rb
111
- - lib/cell/migration.rb
112
- - lib/cell/model_extensions.rb
113
107
  - lib/cell/railtie.rb
114
108
  - lib/cell/sanity_check.rb
115
109
  - lib/cell/schema.rb
@@ -138,7 +132,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
138
132
  version: '0'
139
133
  requirements: []
140
134
  rubyforge_project:
141
- rubygems_version: 2.5.1
135
+ rubygems_version: 2.7.8
142
136
  signing_key:
143
137
  specification_version: 4
144
138
  summary: Provides isolation and tenancy for Rails
@@ -1,32 +0,0 @@
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