cell 0.1.2 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 688c58ded30105abbc900e3b92d524549b860812
4
- data.tar.gz: e176f9fc293ad7027e53052a09561e26e11f1091
2
+ SHA256:
3
+ metadata.gz: bb2eae73c6d15e47ad6f4ea4a75c768d354f34f93f8e68c7d5c873a8dc8ed7d6
4
+ data.tar.gz: 43176f41eb4872ee5d963066f666873d1264a7e8e7b20d10a2467190ada19bae
5
5
  SHA512:
6
- metadata.gz: ec3f3baee67fa98496dabaf50554f5bcfe4b230d0ae6c5d5eba21d00635e5cfeaed3156e233cce8e31289ae73a012df07090649c93e7d7b23bbc8aa95384901d
7
- data.tar.gz: 89ef969c710585e9afb2b43ae8ef8eecc343213586e38eaae9a6a463aef45fdf4033f044781243e949fec595821c8ac2f0466c8e00d3af30ef65c182e227ed16
6
+ metadata.gz: a4fe6e8dc968592539f0eeeaecd2584f61c7b2ffc3f3ab6fe6b69d0fd834cbc1745776f1038b04b34d3be8e38e3394dc79fccb624550100c4c0a0f8e9c1e5cfe
7
+ data.tar.gz: 45b12e68c347eb6a72ef77093227b838b38fcb7a3ba24309927657b31ee85df95896ea155a9289b5dd996d796a8b89eba0da19b4d4d6552c1cf9362cd0c03b99
data/README.md CHANGED
@@ -13,7 +13,7 @@ particular job adapter), except for one place: PostgreSQL. I think PostgreSQL i
13
13
  deployed database with the features required to make a reasonable implementation (namely: schemas,
14
14
  roles, row and column level security.)
15
15
 
16
- **Cell aggressively uses Ruby 2.3, PostgreSQL 9.6 and Rails 5**. This means it's probably more
16
+ **Cell aggressively uses Ruby 2.5, PostgreSQL 10.5 and Rails 5.2**. This means it's probably more
17
17
  appropriate for greenfield projects than bolting on to an existing application on an older stack.
18
18
 
19
19
  Cell moves forward quickly: Don't expect to be able to hang back on an old version of Rails and get
data/cell.gemspec CHANGED
@@ -26,12 +26,12 @@ Gem::Specification.new do |spec|
26
26
  spec.require_paths = ["lib"]
27
27
 
28
28
  # The parts of the Rails stack we care about
29
- spec.add_dependency "rails", "~> 5.0.0", ">= 5.0.0.1"
29
+ spec.add_dependency "rails", "~> 5.2.0"
30
30
 
31
31
  # Our hard dependency on PostgreSQL
32
- spec.add_dependency "pg", "~> 0.18.4"
32
+ spec.add_dependency "pg", "~> 1.1.3"
33
33
 
34
- spec.add_development_dependency "minitest", "~> 5.9.0"
35
- spec.add_development_dependency "bundler", "~> 1.12"
36
- spec.add_development_dependency "rake", "~> 11.0"
34
+ spec.add_development_dependency "minitest", "~> 5.11"
35
+ spec.add_development_dependency "bundler", "~> 1.16"
36
+ spec.add_development_dependency "rake", "~> 12.3"
37
37
  end
@@ -36,200 +36,203 @@ module Cell
36
36
 
37
37
 
38
38
  def self.function_definition
39
- File.read(__FILE__).split(/^__END__$/, 2)[-1]
39
+ FUNCTION_DEFINITION
40
40
  end
41
41
  end
42
42
  end
43
43
 
44
- # The following function was written by Melvin Davidson, originally based on
45
- # the work of Emanuel '3manuek', posted at:
46
- # https://wiki.postgresql.org/wiki/Clone_schema
47
- __END__
48
- -- Function: clone_schema(text, text)
49
-
50
- -- DROP FUNCTION clone_schema(text, text);
51
-
52
- CREATE OR REPLACE FUNCTION public.clone_schema(
53
- source_schema text,
54
- dest_schema text,
55
- include_recs boolean)
56
- RETURNS void AS
57
- $BODY$
58
-
59
- -- This function will clone all sequences, tables, data, views & functions from any existing schema to a new one
60
- -- SAMPLE CALL:
61
- -- SELECT clone_schema('public', 'new_schema', TRUE);
62
-
63
- DECLARE
64
- src_oid oid;
65
- tbl_oid oid;
66
- func_oid oid;
67
- object text;
68
- buffer text;
69
- srctbl text;
70
- default_ text;
71
- column_ text;
72
- qry text;
73
- dest_qry text;
74
- v_def text;
75
- seqval bigint;
76
- sq_last_value bigint;
77
- sq_max_value bigint;
78
- sq_start_value bigint;
79
- sq_increment_by bigint;
80
- sq_min_value bigint;
81
- sq_cache_value bigint;
82
- sq_log_cnt bigint;
83
- sq_is_called boolean;
84
- sq_is_cycled boolean;
85
- sq_cycled char(10);
86
-
87
- BEGIN
88
-
89
- -- Check that source_schema exists
90
- SELECT oid INTO src_oid
91
- FROM pg_namespace
92
- WHERE nspname = quote_ident(source_schema);
93
- IF NOT FOUND
94
- THEN
95
- RAISE NOTICE 'source schema % does not exist!', source_schema;
96
- RETURN ;
97
- END IF;
98
-
99
- -- Check that dest_schema does not yet exist
100
- PERFORM nspname
101
- FROM pg_namespace
102
- WHERE nspname = quote_ident(dest_schema);
103
- IF FOUND
104
- THEN
105
- RAISE NOTICE 'dest schema % already exists!', dest_schema;
106
- RETURN ;
107
- END IF;
108
-
109
- EXECUTE 'CREATE SCHEMA ' || quote_ident(dest_schema) ;
110
-
111
- -- Create sequences
112
- -- TODO: Find a way to make this sequence's owner is the correct table.
113
- FOR object IN
114
- SELECT sequence_name::text
115
- FROM information_schema.sequences
116
- WHERE sequence_schema = quote_ident(source_schema)
117
- LOOP
118
- EXECUTE 'CREATE SEQUENCE ' || quote_ident(dest_schema) || '.' || quote_ident(object);
119
- srctbl := quote_ident(source_schema) || '.' || quote_ident(object);
120
-
121
- EXECUTE 'SELECT last_value, max_value, start_value, increment_by, min_value, cache_value, log_cnt, is_cycled, is_called
122
- FROM ' || quote_ident(source_schema) || '.' || quote_ident(object) || ';'
123
- INTO sq_last_value, sq_max_value, sq_start_value, sq_increment_by, sq_min_value, sq_cache_value, sq_log_cnt, sq_is_cycled, sq_is_called ;
124
-
125
- IF sq_is_cycled
126
- THEN
127
- sq_cycled := 'CYCLE';
128
- ELSE
129
- sq_cycled := 'NO CYCLE';
130
- END IF;
131
-
132
- EXECUTE 'ALTER SEQUENCE ' || quote_ident(dest_schema) || '.' || quote_ident(object)
133
- || ' INCREMENT BY ' || sq_increment_by
134
- || ' MINVALUE ' || sq_min_value
135
- || ' MAXVALUE ' || sq_max_value
136
- || ' START WITH ' || sq_start_value
137
- || ' RESTART ' || sq_min_value
138
- || ' CACHE ' || sq_cache_value
139
- || sq_cycled || ' ;' ;
140
-
141
- buffer := quote_ident(dest_schema) || '.' || quote_ident(object);
142
- IF include_recs
44
+ unless Cell::CloneSchema.const_defined?(:FUNCTION_DEFINITION)
45
+ # The following function was written by Melvin Davidson, originally based on the work of '3manuek',
46
+ # posted at: https://wiki.postgresql.org/wiki/Clone_schema
47
+
48
+ Cell::CloneSchema.const_set :FUNCTION_DEFINITION, <<~PGSQL
49
+ -- Function: clone_schema(text, text)
50
+
51
+ -- DROP FUNCTION clone_schema(text, text);
52
+
53
+ CREATE OR REPLACE FUNCTION public.clone_schema(
54
+ source_schema text,
55
+ dest_schema text,
56
+ include_recs boolean)
57
+ RETURNS void AS
58
+ $BODY$
59
+
60
+ -- This function will clone all sequences, tables, data, views & functions from any existing schema to a new one
61
+ -- SAMPLE CALL:
62
+ -- SELECT clone_schema('public', 'new_schema', TRUE);
63
+
64
+ DECLARE
65
+ src_oid oid;
66
+ tbl_oid oid;
67
+ func_oid oid;
68
+ object text;
69
+ buffer text;
70
+ srctbl text;
71
+ default_ text;
72
+ column_ text;
73
+ qry text;
74
+ dest_qry text;
75
+ v_def text;
76
+ seqval bigint;
77
+ sq_last_value bigint;
78
+ sq_max_value bigint;
79
+ sq_start_value bigint;
80
+ sq_increment_by bigint;
81
+ sq_min_value bigint;
82
+ sq_cache_value bigint;
83
+ sq_log_cnt bigint;
84
+ sq_is_called boolean;
85
+ sq_is_cycled boolean;
86
+ sq_cycled char(10);
87
+
88
+ BEGIN
89
+
90
+ -- Check that source_schema exists
91
+ SELECT oid INTO src_oid
92
+ FROM pg_namespace
93
+ WHERE nspname = quote_ident(source_schema);
94
+ IF NOT FOUND
143
95
  THEN
144
- EXECUTE 'SELECT setval( ''' || buffer || ''', ' || sq_last_value || ', ' || sq_is_called || ');' ;
145
- ELSE
146
- EXECUTE 'SELECT setval( ''' || buffer || ''', ' || sq_start_value || ', ' || sq_is_called || ');' ;
147
- END IF;
148
-
149
- END LOOP;
150
-
151
- -- Create tables
152
- FOR object IN
153
- SELECT TABLE_NAME::text
154
- FROM information_schema.tables
155
- WHERE table_schema = quote_ident(source_schema)
156
- AND table_type = 'BASE TABLE'
157
-
158
- LOOP
159
- buffer := quote_ident(dest_schema) || '.' || quote_ident(object);
160
- EXECUTE 'CREATE TABLE ' || buffer || ' (LIKE ' || quote_ident(source_schema) || '.' || quote_ident(object)
161
- || ' INCLUDING ALL)';
162
-
163
- IF include_recs
164
- THEN
165
- -- Insert records from source table
166
- EXECUTE 'INSERT INTO ' || buffer || ' SELECT * FROM ' || quote_ident(source_schema) || '.' || quote_ident(object) || ';';
167
- END IF;
168
-
169
- FOR column_, default_ IN
170
- SELECT column_name::text,
171
- REPLACE(column_default::text, source_schema, dest_schema)
172
- FROM information_schema.COLUMNS
173
- WHERE table_schema = dest_schema
174
- AND TABLE_NAME = object
175
- AND column_default LIKE 'nextval(%' || quote_ident(source_schema) || '%::regclass)'
176
- LOOP
177
- EXECUTE 'ALTER TABLE ' || buffer || ' ALTER COLUMN ' || column_ || ' SET DEFAULT ' || default_;
178
- END LOOP;
179
-
180
- END LOOP;
181
-
182
- -- add FK constraint
183
- FOR qry IN
184
- SELECT 'ALTER TABLE ' || quote_ident(dest_schema) || '.' || quote_ident(rn.relname)
185
- || ' ADD CONSTRAINT ' || quote_ident(ct.conname) || ' ' || pg_get_constraintdef(ct.oid) || ';'
186
- FROM pg_constraint ct
187
- JOIN pg_class rn ON rn.oid = ct.conrelid
188
- WHERE connamespace = src_oid
189
- AND rn.relkind = 'r'
190
- AND ct.contype = 'f'
191
-
192
- LOOP
193
- EXECUTE qry;
194
-
195
- END LOOP;
196
-
197
-
198
- -- Create views
199
- FOR object IN
200
- SELECT table_name::text,
201
- view_definition
202
- FROM information_schema.views
203
- WHERE table_schema = quote_ident(source_schema)
204
-
205
- LOOP
206
- buffer := dest_schema || '.' || quote_ident(object);
207
- SELECT view_definition INTO v_def
208
- FROM information_schema.views
209
- WHERE table_schema = quote_ident(source_schema)
210
- AND table_name = quote_ident(object);
211
-
212
- EXECUTE 'CREATE OR REPLACE VIEW ' || buffer || ' AS ' || v_def || ';' ;
213
-
214
- END LOOP;
215
-
216
- -- Create functions
217
- FOR func_oid IN
218
- SELECT oid
219
- FROM pg_proc
220
- WHERE pronamespace = src_oid
221
-
222
- LOOP
223
- SELECT pg_get_functiondef(func_oid) INTO qry;
224
- SELECT replace(qry, source_schema, dest_schema) INTO dest_qry;
225
- EXECUTE dest_qry;
226
-
227
- END LOOP;
228
-
229
- RETURN;
230
-
231
- END;
232
-
233
- $BODY$
234
- LANGUAGE plpgsql VOLATILE
235
- COST 100;
96
+ RAISE NOTICE 'source schema % does not exist!', source_schema;
97
+ RETURN ;
98
+ END IF;
99
+
100
+ -- Check that dest_schema does not yet exist
101
+ PERFORM nspname
102
+ FROM pg_namespace
103
+ WHERE nspname = quote_ident(dest_schema);
104
+ IF FOUND
105
+ THEN
106
+ RAISE NOTICE 'dest schema % already exists!', dest_schema;
107
+ RETURN ;
108
+ END IF;
109
+
110
+ EXECUTE 'CREATE SCHEMA ' || quote_ident(dest_schema) ;
111
+
112
+ -- Create sequences
113
+ -- TODO: Find a way to make this sequence's owner is the correct table.
114
+ FOR object IN
115
+ SELECT sequence_name::text
116
+ FROM information_schema.sequences
117
+ WHERE sequence_schema = quote_ident(source_schema)
118
+ LOOP
119
+ EXECUTE 'CREATE SEQUENCE ' || quote_ident(dest_schema) || '.' || quote_ident(object);
120
+ srctbl := quote_ident(source_schema) || '.' || quote_ident(object);
121
+
122
+ EXECUTE 'SELECT last_value, max_value, start_value, increment_by, min_value, cache_value, log_cnt, is_cycled, is_called
123
+ FROM ' || quote_ident(source_schema) || '.' || quote_ident(object) || ';'
124
+ INTO sq_last_value, sq_max_value, sq_start_value, sq_increment_by, sq_min_value, sq_cache_value, sq_log_cnt, sq_is_cycled, sq_is_called ;
125
+
126
+ IF sq_is_cycled
127
+ THEN
128
+ sq_cycled := 'CYCLE';
129
+ ELSE
130
+ sq_cycled := 'NO CYCLE';
131
+ END IF;
132
+
133
+ EXECUTE 'ALTER SEQUENCE ' || quote_ident(dest_schema) || '.' || quote_ident(object)
134
+ || ' INCREMENT BY ' || sq_increment_by
135
+ || ' MINVALUE ' || sq_min_value
136
+ || ' MAXVALUE ' || sq_max_value
137
+ || ' START WITH ' || sq_start_value
138
+ || ' RESTART ' || sq_min_value
139
+ || ' CACHE ' || sq_cache_value
140
+ || sq_cycled || ' ;' ;
141
+
142
+ buffer := quote_ident(dest_schema) || '.' || quote_ident(object);
143
+ IF include_recs
144
+ THEN
145
+ EXECUTE 'SELECT setval( ''' || buffer || ''', ' || sq_last_value || ', ' || sq_is_called || ');' ;
146
+ ELSE
147
+ EXECUTE 'SELECT setval( ''' || buffer || ''', ' || sq_start_value || ', ' || sq_is_called || ');' ;
148
+ END IF;
149
+
150
+ END LOOP;
151
+
152
+ -- Create tables
153
+ FOR object IN
154
+ SELECT TABLE_NAME::text
155
+ FROM information_schema.tables
156
+ WHERE table_schema = quote_ident(source_schema)
157
+ AND table_type = 'BASE TABLE'
158
+
159
+ LOOP
160
+ buffer := quote_ident(dest_schema) || '.' || quote_ident(object);
161
+ EXECUTE 'CREATE TABLE ' || buffer || ' (LIKE ' || quote_ident(source_schema) || '.' || quote_ident(object)
162
+ || ' INCLUDING ALL)';
163
+
164
+ IF include_recs
165
+ THEN
166
+ -- Insert records from source table
167
+ EXECUTE 'INSERT INTO ' || buffer || ' SELECT * FROM ' || quote_ident(source_schema) || '.' || quote_ident(object) || ';';
168
+ END IF;
169
+
170
+ FOR column_, default_ IN
171
+ SELECT column_name::text,
172
+ REPLACE(column_default::text, source_schema, dest_schema)
173
+ FROM information_schema.COLUMNS
174
+ WHERE table_schema = dest_schema
175
+ AND TABLE_NAME = object
176
+ AND column_default LIKE 'nextval(%' || quote_ident(source_schema) || '%::regclass)'
177
+ LOOP
178
+ EXECUTE 'ALTER TABLE ' || buffer || ' ALTER COLUMN ' || column_ || ' SET DEFAULT ' || default_;
179
+ END LOOP;
180
+
181
+ END LOOP;
182
+
183
+ -- add FK constraint
184
+ FOR qry IN
185
+ SELECT 'ALTER TABLE ' || quote_ident(dest_schema) || '.' || quote_ident(rn.relname)
186
+ || ' ADD CONSTRAINT ' || quote_ident(ct.conname) || ' ' || pg_get_constraintdef(ct.oid) || ';'
187
+ FROM pg_constraint ct
188
+ JOIN pg_class rn ON rn.oid = ct.conrelid
189
+ WHERE connamespace = src_oid
190
+ AND rn.relkind = 'r'
191
+ AND ct.contype = 'f'
192
+
193
+ LOOP
194
+ EXECUTE qry;
195
+
196
+ END LOOP;
197
+
198
+
199
+ -- Create views
200
+ FOR object IN
201
+ SELECT table_name::text,
202
+ view_definition
203
+ FROM information_schema.views
204
+ WHERE table_schema = quote_ident(source_schema)
205
+
206
+ LOOP
207
+ buffer := dest_schema || '.' || quote_ident(object);
208
+ SELECT view_definition INTO v_def
209
+ FROM information_schema.views
210
+ WHERE table_schema = quote_ident(source_schema)
211
+ AND table_name = quote_ident(object);
212
+
213
+ EXECUTE 'CREATE OR REPLACE VIEW ' || buffer || ' AS ' || v_def || ';' ;
214
+
215
+ END LOOP;
216
+
217
+ -- Create functions
218
+ FOR func_oid IN
219
+ SELECT oid
220
+ FROM pg_proc
221
+ WHERE pronamespace = src_oid
222
+
223
+ LOOP
224
+ SELECT pg_get_functiondef(func_oid) INTO qry;
225
+ SELECT replace(qry, source_schema, dest_schema) INTO dest_qry;
226
+ EXECUTE dest_qry;
227
+
228
+ END LOOP;
229
+
230
+ RETURN;
231
+
232
+ END;
233
+
234
+ $BODY$
235
+ LANGUAGE plpgsql VOLATILE
236
+ COST 100;
237
+ PGSQL
238
+ end
data/lib/cell/console.rb CHANGED
@@ -2,18 +2,25 @@ module Cell
2
2
  module Console
3
3
  def self.default_console_tenant
4
4
  if ENV['T'].present?
5
- Cell::Model.cell_find!(ENV['T'])
5
+ Model.cell_find!(ENV['T'])
6
6
  elsif Rails.env.development? && ENV['T'] != ''
7
- Cell::Model.first
7
+ Model.first
8
8
  end
9
+ # These are so 'rails c' still works before the DB has been created, or the
10
+ # Model class's table does not exist before db:migrate.
11
+ rescue ::ActiveRecord::NoDatabaseError
12
+ nil
13
+ rescue ::ActiveRecord::StatementInvalid => e
14
+ raise unless e.cause.is_a?(::PG::UndefinedTable)
15
+ nil
9
16
  end
10
17
 
11
18
  def self.configure!
12
19
  if (t = default_console_tenant)
13
- ::Cell::Model.set!(t)
20
+ Model.set!(t)
14
21
  end
15
22
  end
16
23
  end
17
24
  end
18
25
 
19
- ::Cell::Console.configure!
26
+ Cell::Console.configure!
data/lib/cell/context.rb CHANGED
@@ -7,7 +7,7 @@ module Cell
7
7
 
8
8
  module ClassMethods
9
9
  def self.extended(cls)
10
- cls.include ActiveSupport::Callbacks
10
+ cls.include ::ActiveSupport::Callbacks
11
11
  cls.define_callbacks :set_tenant, :use_tenant
12
12
  end
13
13
 
@@ -24,7 +24,7 @@ module Cell
24
24
  end
25
25
 
26
26
  def set!(tenant, exclusive: false)
27
- ::Cell::Meta.with_schema(tenant.schema_name, exclusive: exclusive)
27
+ Meta.with_schema(tenant.schema_name, exclusive: exclusive)
28
28
  set_current(tenant)
29
29
  tenant.run_callbacks :set_tenant
30
30
  tenant
@@ -35,14 +35,14 @@ module Cell
35
35
  saved_tenant = current
36
36
 
37
37
  if tenant
38
- ::Cell::Meta.with_schema(tenant.schema_name, exclusive: exclusive) do
38
+ Meta.with_schema(tenant.schema_name, exclusive: exclusive) do
39
39
  set_current(tenant)
40
40
  tenant.run_callbacks :use_tenant do
41
- yield Cell::Model.current
41
+ yield Model.current
42
42
  end
43
43
  end
44
44
  else
45
- ::Cell::Meta.with_global_schema do
45
+ Meta.with_global_schema do
46
46
  set_current(nil)
47
47
  yield nil
48
48
  end
@@ -55,7 +55,7 @@ module Cell
55
55
 
56
56
  private
57
57
  def set_current(tenant)
58
- unless tenant.nil? || tenant.is_a?(Cell::Model)
58
+ unless tenant.nil? || tenant.is_a?(Model)
59
59
  fail ArgumentError, "Invalid tenant: #{tenant.inspect}"
60
60
  end
61
61
 
@@ -68,7 +68,7 @@ module Cell
68
68
  end
69
69
 
70
70
  def set!(tenant)
71
- Cell::Model.set!(tenant)
71
+ Model.set!(tenant)
72
72
  end
73
73
 
74
74
  def current?
@@ -0,0 +1,33 @@
1
+ module Cell
2
+ module Ext
3
+ module ActiveJob
4
+ KEY = :'Cell.cell_id'
5
+
6
+ def serialize
7
+ if (current_id = Model.current&.cell_id)
8
+ super.merge(KEY => current_id)
9
+ else
10
+ super
11
+ end
12
+ end
13
+
14
+ def deserialize(job_data)
15
+ if job_data.key?(KEY)
16
+ self.cell_tenant = Model.cell_find(job_data[KEY])
17
+ end
18
+ super
19
+ end
20
+
21
+ def self.prepended(cls)
22
+ cls.send(:attr_accessor, :cell_tenant)
23
+ cls.around_perform do |job, block|
24
+ Model.use(job.cell_tenant, &block)
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+
31
+ ActiveSupport::on_load(:active_job) do
32
+ ActiveJob::Base.prepend(Cell::Ext::ActiveJob)
33
+ end
@@ -0,0 +1,81 @@
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 Ext
16
+ module ActiveRecord
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 Association
20
+ def load_target
21
+ owner._activate_tenant do
22
+ super
23
+ end
24
+ end
25
+ end
26
+
27
+ module ClassMethods
28
+ def global_model?
29
+ Meta.global_model?(self)
30
+ end
31
+
32
+ # When a model's schema is lazy-loaded, we want to make sure our view of the DB allows us
33
+ # to get introspection data.
34
+ def load_schema!
35
+ Meta.with_structural_schema do
36
+ return super
37
+ end
38
+ end
39
+ end
40
+
41
+ attr_reader :tenant
42
+
43
+ def _assign_tenant
44
+ unless self.class.global_model?
45
+ @tenant = Model.current
46
+ end
47
+ end
48
+
49
+ def _activate_tenant(&block)
50
+ if tenant && tenant != Model.current
51
+ tenant.use(&block)
52
+ else
53
+ yield
54
+ end
55
+ end
56
+
57
+ # undocumented, but makes sure validations are ran in the context of the object's tenant,
58
+ # e.g., uniqueness.
59
+ def run_validations!
60
+ _activate_tenant do
61
+ super
62
+ end
63
+ end
64
+
65
+ def self.prepended(cls)
66
+ class << cls
67
+ prepend ClassMethods
68
+ end
69
+
70
+ cls.after_initialize :_assign_tenant
71
+ cls.around_save :_activate_tenant
72
+ end
73
+ end
74
+ end
75
+ end
76
+
77
+ ActiveSupport.on_load(:active_record) do
78
+ ActiveRecord::Base.prepend(Cell::Ext::ActiveRecord)
79
+ ActiveRecord::Associations::Association.prepend(Cell::Ext::ActiveRecord::Association)
80
+ ActiveRecord::Associations::CollectionAssociation.prepend(Cell::Ext::ActiveRecord::Association)
81
+ end