cell 0.1.2 → 0.2.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.
@@ -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