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 +5 -5
- data/README.md +1 -1
- data/cell.gemspec +5 -5
- data/lib/cell/clone_schema.rb +195 -192
- data/lib/cell/console.rb +11 -4
- data/lib/cell/context.rb +7 -7
- data/lib/cell/ext/active_job.rb +33 -0
- data/lib/cell/ext/active_record.rb +81 -0
- data/lib/cell/ext/migration.rb +198 -0
- data/lib/cell/meta.rb +3 -2
- data/lib/cell/railtie.rb +6 -7
- data/lib/cell/sanity_check.rb +9 -10
- data/lib/cell/schema.rb +8 -13
- data/lib/cell/tenant.rb +13 -12
- data/lib/cell/version.rb +1 -1
- metadata +16 -22
- data/lib/cell/active_job.rb +0 -32
- data/lib/cell/migration.rb +0 -201
- data/lib/cell/model_extensions.rb +0 -85
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: bb2eae73c6d15e47ad6f4ea4a75c768d354f34f93f8e68c7d5c873a8dc8ed7d6
|
4
|
+
data.tar.gz: 43176f41eb4872ee5d963066f666873d1264a7e8e7b20d10a2467190ada19bae
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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.
|
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.
|
29
|
+
spec.add_dependency "rails", "~> 5.2.0"
|
30
30
|
|
31
31
|
# Our hard dependency on PostgreSQL
|
32
|
-
spec.add_dependency "pg", "~>
|
32
|
+
spec.add_dependency "pg", "~> 1.1.3"
|
33
33
|
|
34
|
-
spec.add_development_dependency "minitest", "~> 5.
|
35
|
-
spec.add_development_dependency "bundler", "~> 1.
|
36
|
-
spec.add_development_dependency "rake", "~>
|
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
|
data/lib/cell/clone_schema.rb
CHANGED
@@ -36,200 +36,203 @@ module Cell
|
|
36
36
|
|
37
37
|
|
38
38
|
def self.function_definition
|
39
|
-
|
39
|
+
FUNCTION_DEFINITION
|
40
40
|
end
|
41
41
|
end
|
42
42
|
end
|
43
43
|
|
44
|
-
|
45
|
-
# the work of
|
46
|
-
# https://wiki.postgresql.org/wiki/Clone_schema
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
--
|
61
|
-
--
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
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
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
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
|
-
|
5
|
+
Model.cell_find!(ENV['T'])
|
6
6
|
elsif Rails.env.development? && ENV['T'] != ''
|
7
|
-
|
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
|
-
|
20
|
+
Model.set!(t)
|
14
21
|
end
|
15
22
|
end
|
16
23
|
end
|
17
24
|
end
|
18
25
|
|
19
|
-
|
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
|
-
|
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
|
-
|
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
|
41
|
+
yield Model.current
|
42
42
|
end
|
43
43
|
end
|
44
44
|
else
|
45
|
-
|
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?(
|
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
|
-
|
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
|