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.
@@ -1,201 +0,0 @@
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 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?
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
102
- # SchemaStatements
103
- intercept %i(add_belongs_to add_column add_foreign_key add_index
104
- add_index_sort_order add_reference add_timestamps
105
- change_column change_column_default change_column_null
106
- change_table change_table_comment create_join_table
107
- create_table drop_join_table drop_table
108
- initialize_schema_migrations_table remove_belongs_to
109
- remove_column remove_columns remove_foreign_key remove_index
110
- remove_reference remove_timestamps rename_column
111
- rename_column_indexes rename_index rename_table
112
- rename_table_indexes table_alias_for table_comment) +
113
- %i(enable_extension disable_extension truncate) +
114
- %i(execute)
115
-
116
- def global_schema
117
- Cell::Meta.global_schema
118
- end
119
-
120
- def prototype_schema
121
- Cell::Meta.prototype_schema
122
- end
123
-
124
- def tenant_schema
125
- target.schema_name
126
- end
127
-
128
- def target
129
- Cell::Model.current
130
- end
131
-
132
- def targeted?
133
- !! target
134
- end
135
-
136
- # This is our super-special initialization function.
137
- def initialize_cell!
138
- CloneSchema.install_function!
139
- execute "CREATE SCHEMA #{connection.quote_schema_name(prototype_schema)}"
140
- end
141
-
142
- def exec_migration(con, direction)
143
- if ! targeted?
144
- with_context(:global, global_schema) do
145
- super
146
- end
147
-
148
- with_context(:prototype, prototype_schema) do
149
- super
150
- end
151
- else
152
- with_context(:target, tenant_schema, exclusive: true) do
153
- super
154
- end
155
- end
156
- end
157
-
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)
162
- end
163
-
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
200
- end
201
- end
@@ -1,85 +0,0 @@
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!