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