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.
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