cell 0.1.0 → 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +5 -9
- data/LICENSE.txt +1 -1
- data/README.md +61 -9
- data/Rakefile +29 -2
- data/bin/test +9 -0
- data/cell.gemspec +10 -4
- data/lib/cell/clone_schema.rb +233 -0
- data/lib/cell/configure.rb +70 -0
- data/lib/cell/console.rb +25 -0
- data/lib/cell/context.rb +61 -0
- data/lib/cell/data_directory.rb +42 -0
- data/lib/cell/migration.rb +114 -0
- data/lib/cell/model.rb +37 -0
- data/lib/cell/railtie.rb +17 -0
- data/lib/cell/sanity_check.rb +66 -0
- data/lib/cell/schema.rb +64 -0
- data/lib/cell/sidekiq.rb +58 -0
- data/lib/cell/tasks/cell.rake +56 -0
- data/lib/cell/version.rb +1 -1
- data/lib/cell/with_schema.rb +30 -0
- data/lib/cell.rb +5 -3
- data/lib/dump/tenant_routing_manager.rb +25 -0
- data/lib/dump/tenant_url_options.rb +17 -0
- metadata +66 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 85f49de6468edae5eac2de24fde919be5eb2af39
|
4
|
+
data.tar.gz: d369c389371266a1cbb0d36268ae76608e9c19e8
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6f1f93638f278f5797a2c76d5f4d74a9e8c139462417afad7b38d7ca34f51734c6edd047c3afea25f43082ddb6c99f0148b5002f54f1911b021c7aeb9f373d25
|
7
|
+
data.tar.gz: fc16f2aa775623f1904a11a61edc06a11cbc1d2a4fe3d814f2684f462733d7c64919a9ffe9b841f5b987aa63483b3ef8d7b8ce494f40fdcf2aceb12c011fb4c9
|
data/.gitignore
CHANGED
data/LICENSE.txt
CHANGED
data/README.md
CHANGED
@@ -1,8 +1,44 @@
|
|
1
1
|
# Cell
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
3
|
+
Cell provides tenancy and customer isolation for Rails. It tries to be a
|
4
|
+
simpler and more thorough implementation of things like Apartment.
|
5
|
+
|
6
|
+
The core of Cell was written years ago for Rails 1.2, before Apartment was
|
7
|
+
available, and has been developed inside an application ever since. We've
|
8
|
+
aggressively moved it along with the ecosystem, and it still feels more modern
|
9
|
+
than other implementations we've used. We're in the process of making it a
|
10
|
+
more generalized gem. Things are a bit messy now.
|
11
|
+
|
12
|
+
Cell tries to hook into Rails at the highest level available (e.g., ActiveJob
|
13
|
+
instead of a particular job adapter), except for one place: PostgreSQL. I
|
14
|
+
think PostgreSQL is the only widely deployed database with the features required
|
15
|
+
to make a reasonable implementation (namely: schemas, roles, row and column
|
16
|
+
level security.)
|
17
|
+
|
18
|
+
**Cell aggressively uses Ruby 2.3, PostgreSQL 9.6 and Rails 5**. This means
|
19
|
+
it's probably more appropriate for greenfield projects than bolting on to an
|
20
|
+
existing application on an older stack.
|
21
|
+
|
22
|
+
Cell moves forward quickly: Don't expect to be able to hang back on an old
|
23
|
+
version of Rails and get new features in Cell. We version accordingly not to
|
24
|
+
break things, but we don't back-port to old Rubies, old PostgreSQLs, or old
|
25
|
+
Rails versions.
|
26
|
+
|
27
|
+
The aforementioned Apartment gem looks like perfectly viable solution for
|
28
|
+
projects with other constraints.
|
29
|
+
|
30
|
+
What it handles:
|
31
|
+
|
32
|
+
[x] ActiveRecord (including migrations and global tables)
|
33
|
+
[ ] ActionController (An integration point for you)
|
34
|
+
[ ] ActiveJob (we're transitioning a Sidekiq adapter to ActiveJob)
|
35
|
+
[ ] ActionMailer (Your URIs will work)
|
36
|
+
[ ] ActionView (Caching is automatically keyed on the Cell)
|
37
|
+
[ ] Redis (optional, via redis-namespace)
|
38
|
+
[x] Data Directories
|
39
|
+
[x] Rake (e.g., tasks which run over each Cell)
|
40
|
+
|
41
|
+
The rest of this README will be updated as the code is extracted.
|
6
42
|
|
7
43
|
## Installation
|
8
44
|
|
@@ -20,22 +56,38 @@ Or install it yourself as:
|
|
20
56
|
|
21
57
|
$ gem install cell
|
22
58
|
|
59
|
+
## Tests
|
60
|
+
|
61
|
+
The application Cell was ripped out of had heavy test-coverage of the
|
62
|
+
functionality Cell provides, but at the wrong level (e.g., our model, not the
|
63
|
+
Cell behavior which this became). It's proven impossible to extract anything
|
64
|
+
workable out of these into this gem, but we're working on starting from
|
65
|
+
scratch.
|
66
|
+
|
67
|
+
|
23
68
|
## Usage
|
24
69
|
|
25
|
-
|
70
|
+
FIXME: Write this!
|
26
71
|
|
27
72
|
## Development
|
28
73
|
|
29
|
-
After checking out the repo, run `bin/setup` to install dependencies.
|
74
|
+
After checking out the repo, run `bin/setup` to install dependencies.
|
75
|
+
You can also run `bin/console` for an interactive prompt that will allow you
|
76
|
+
to experiment.
|
30
77
|
|
31
|
-
To install this gem onto your local machine, run `bundle exec rake install`. To
|
78
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To
|
79
|
+
release a new version, update the version number in `version.rb`, and then
|
80
|
+
run `bundle exec rake release`, which will create a git tag for the version,
|
81
|
+
push git commits and tags, and push the `.gem` file to
|
82
|
+
[rubygems.org](https://rubygems.org).
|
32
83
|
|
33
84
|
## Contributing
|
34
85
|
|
35
|
-
Bug reports and pull requests are welcome on GitHub at
|
86
|
+
Bug reports and pull requests are welcome on GitHub at
|
87
|
+
https://github.com/metermd/cell.
|
36
88
|
|
37
89
|
|
38
90
|
## License
|
39
91
|
|
40
|
-
The gem is available as open source under the terms of the
|
41
|
-
|
92
|
+
The gem is available as open source under the terms of the
|
93
|
+
[MIT License](http://opensource.org/licenses/MIT).
|
data/Rakefile
CHANGED
@@ -1,2 +1,29 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
begin
|
2
|
+
require 'bundler/setup'
|
3
|
+
rescue LoadError
|
4
|
+
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
5
|
+
end
|
6
|
+
|
7
|
+
require 'rdoc/task'
|
8
|
+
|
9
|
+
RDoc::Task.new(:rdoc) do |rdoc|
|
10
|
+
rdoc.rdoc_dir = 'rdoc'
|
11
|
+
rdoc.title = 'Testface'
|
12
|
+
rdoc.options << '--line-numbers'
|
13
|
+
rdoc.rdoc_files.include('README.md')
|
14
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
15
|
+
end
|
16
|
+
|
17
|
+
|
18
|
+
require 'bundler/gem_tasks'
|
19
|
+
require 'rake/testtask'
|
20
|
+
|
21
|
+
Rake::TestTask.new(:test) do |t|
|
22
|
+
t.libs << 'lib'
|
23
|
+
t.libs << 'test'
|
24
|
+
t.pattern = 'test/**/*_test.rb'
|
25
|
+
t.verbose = false
|
26
|
+
end
|
27
|
+
|
28
|
+
|
29
|
+
task default: :test
|
data/bin/test
ADDED
data/cell.gemspec
CHANGED
@@ -1,6 +1,5 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
1
|
+
$:.push File.expand_path("../lib", __FILE__)
|
2
|
+
|
4
3
|
require 'cell/version'
|
5
4
|
|
6
5
|
Gem::Specification.new do |spec|
|
@@ -26,6 +25,13 @@ Gem::Specification.new do |spec|
|
|
26
25
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
27
26
|
spec.require_paths = ["lib"]
|
28
27
|
|
28
|
+
# The parts of the Rails stack we care about
|
29
|
+
spec.add_dependency "rails", "~> 5.0.0", ">= 5.0.0.1"
|
30
|
+
|
31
|
+
# Our hard dependency on PostgreSQL
|
32
|
+
spec.add_dependency "pg", "~> 0.18.4"
|
33
|
+
|
34
|
+
spec.add_development_dependency "minitest", "~> 5.9.0"
|
29
35
|
spec.add_development_dependency "bundler", "~> 1.12"
|
30
|
-
spec.add_development_dependency "rake",
|
36
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
31
37
|
end
|
@@ -0,0 +1,233 @@
|
|
1
|
+
require 'active_record/schema_migration'
|
2
|
+
require 'cell/with_schema'
|
3
|
+
|
4
|
+
module Cell
|
5
|
+
module CloneSchema
|
6
|
+
module_function
|
7
|
+
def clone_schema(con, src, dst)
|
8
|
+
sql = <<~SQL
|
9
|
+
SELECT public.clone_schema(#{con.quote(src)}, #{con.quote(dst)}, true);
|
10
|
+
SQL
|
11
|
+
con.execute(sql)
|
12
|
+
end
|
13
|
+
|
14
|
+
|
15
|
+
def copy_schema_migrations_to(dst, connection:)
|
16
|
+
Cell::WithSchema.with_schema(dst, exclusive: true,
|
17
|
+
connection: connection) do
|
18
|
+
::ActiveRecord::SchemaMigration.create_table
|
19
|
+
end
|
20
|
+
|
21
|
+
con = connection
|
22
|
+
sql = "INSERT INTO #{con.quote_schema_name(dst)}.schema_migrations " +
|
23
|
+
"SELECT * from schema_migrations"
|
24
|
+
con.execute(sql)
|
25
|
+
end
|
26
|
+
|
27
|
+
|
28
|
+
def install_function!(con)
|
29
|
+
@created ||= begin
|
30
|
+
con.execute(function_definition)
|
31
|
+
true
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
|
36
|
+
def function_definition
|
37
|
+
File.read(__FILE__).split(/^__END__$/, 2)[-1]
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# The following function was written by Melvin Davidson, originally based on
|
43
|
+
# the work of Emanuel '3manuek', posted at:
|
44
|
+
# https://wiki.postgresql.org/wiki/Clone_schema
|
45
|
+
__END__
|
46
|
+
-- Function: clone_schema(text, text)
|
47
|
+
|
48
|
+
-- DROP FUNCTION clone_schema(text, text);
|
49
|
+
|
50
|
+
CREATE OR REPLACE FUNCTION public.clone_schema(
|
51
|
+
source_schema text,
|
52
|
+
dest_schema text,
|
53
|
+
include_recs boolean)
|
54
|
+
RETURNS void AS
|
55
|
+
$BODY$
|
56
|
+
|
57
|
+
-- This function will clone all sequences, tables, data, views & functions from any existing schema to a new one
|
58
|
+
-- SAMPLE CALL:
|
59
|
+
-- SELECT clone_schema('public', 'new_schema', TRUE);
|
60
|
+
|
61
|
+
DECLARE
|
62
|
+
src_oid oid;
|
63
|
+
tbl_oid oid;
|
64
|
+
func_oid oid;
|
65
|
+
object text;
|
66
|
+
buffer text;
|
67
|
+
srctbl text;
|
68
|
+
default_ text;
|
69
|
+
column_ text;
|
70
|
+
qry text;
|
71
|
+
dest_qry text;
|
72
|
+
v_def text;
|
73
|
+
seqval bigint;
|
74
|
+
sq_last_value bigint;
|
75
|
+
sq_max_value bigint;
|
76
|
+
sq_start_value bigint;
|
77
|
+
sq_increment_by bigint;
|
78
|
+
sq_min_value bigint;
|
79
|
+
sq_cache_value bigint;
|
80
|
+
sq_log_cnt bigint;
|
81
|
+
sq_is_called boolean;
|
82
|
+
sq_is_cycled boolean;
|
83
|
+
sq_cycled char(10);
|
84
|
+
|
85
|
+
BEGIN
|
86
|
+
|
87
|
+
-- Check that source_schema exists
|
88
|
+
SELECT oid INTO src_oid
|
89
|
+
FROM pg_namespace
|
90
|
+
WHERE nspname = quote_ident(source_schema);
|
91
|
+
IF NOT FOUND
|
92
|
+
THEN
|
93
|
+
RAISE NOTICE 'source schema % does not exist!', source_schema;
|
94
|
+
RETURN ;
|
95
|
+
END IF;
|
96
|
+
|
97
|
+
-- Check that dest_schema does not yet exist
|
98
|
+
PERFORM nspname
|
99
|
+
FROM pg_namespace
|
100
|
+
WHERE nspname = quote_ident(dest_schema);
|
101
|
+
IF FOUND
|
102
|
+
THEN
|
103
|
+
RAISE NOTICE 'dest schema % already exists!', dest_schema;
|
104
|
+
RETURN ;
|
105
|
+
END IF;
|
106
|
+
|
107
|
+
EXECUTE 'CREATE SCHEMA ' || quote_ident(dest_schema) ;
|
108
|
+
|
109
|
+
-- Create sequences
|
110
|
+
-- TODO: Find a way to make this sequence's owner is the correct table.
|
111
|
+
FOR object IN
|
112
|
+
SELECT sequence_name::text
|
113
|
+
FROM information_schema.sequences
|
114
|
+
WHERE sequence_schema = quote_ident(source_schema)
|
115
|
+
LOOP
|
116
|
+
EXECUTE 'CREATE SEQUENCE ' || quote_ident(dest_schema) || '.' || quote_ident(object);
|
117
|
+
srctbl := quote_ident(source_schema) || '.' || quote_ident(object);
|
118
|
+
|
119
|
+
EXECUTE 'SELECT last_value, max_value, start_value, increment_by, min_value, cache_value, log_cnt, is_cycled, is_called
|
120
|
+
FROM ' || quote_ident(source_schema) || '.' || quote_ident(object) || ';'
|
121
|
+
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 ;
|
122
|
+
|
123
|
+
IF sq_is_cycled
|
124
|
+
THEN
|
125
|
+
sq_cycled := 'CYCLE';
|
126
|
+
ELSE
|
127
|
+
sq_cycled := 'NO CYCLE';
|
128
|
+
END IF;
|
129
|
+
|
130
|
+
EXECUTE 'ALTER SEQUENCE ' || quote_ident(dest_schema) || '.' || quote_ident(object)
|
131
|
+
|| ' INCREMENT BY ' || sq_increment_by
|
132
|
+
|| ' MINVALUE ' || sq_min_value
|
133
|
+
|| ' MAXVALUE ' || sq_max_value
|
134
|
+
|| ' START WITH ' || sq_start_value
|
135
|
+
|| ' RESTART ' || sq_min_value
|
136
|
+
|| ' CACHE ' || sq_cache_value
|
137
|
+
|| sq_cycled || ' ;' ;
|
138
|
+
|
139
|
+
buffer := quote_ident(dest_schema) || '.' || quote_ident(object);
|
140
|
+
IF include_recs
|
141
|
+
THEN
|
142
|
+
EXECUTE 'SELECT setval( ''' || buffer || ''', ' || sq_last_value || ', ' || sq_is_called || ');' ;
|
143
|
+
ELSE
|
144
|
+
EXECUTE 'SELECT setval( ''' || buffer || ''', ' || sq_start_value || ', ' || sq_is_called || ');' ;
|
145
|
+
END IF;
|
146
|
+
|
147
|
+
END LOOP;
|
148
|
+
|
149
|
+
-- Create tables
|
150
|
+
FOR object IN
|
151
|
+
SELECT TABLE_NAME::text
|
152
|
+
FROM information_schema.tables
|
153
|
+
WHERE table_schema = quote_ident(source_schema)
|
154
|
+
AND table_type = 'BASE TABLE'
|
155
|
+
|
156
|
+
LOOP
|
157
|
+
buffer := quote_ident(dest_schema) || '.' || quote_ident(object);
|
158
|
+
EXECUTE 'CREATE TABLE ' || buffer || ' (LIKE ' || quote_ident(source_schema) || '.' || quote_ident(object)
|
159
|
+
|| ' INCLUDING ALL)';
|
160
|
+
|
161
|
+
IF include_recs
|
162
|
+
THEN
|
163
|
+
-- Insert records from source table
|
164
|
+
EXECUTE 'INSERT INTO ' || buffer || ' SELECT * FROM ' || quote_ident(source_schema) || '.' || quote_ident(object) || ';';
|
165
|
+
END IF;
|
166
|
+
|
167
|
+
FOR column_, default_ IN
|
168
|
+
SELECT column_name::text,
|
169
|
+
REPLACE(column_default::text, source_schema, dest_schema)
|
170
|
+
FROM information_schema.COLUMNS
|
171
|
+
WHERE table_schema = dest_schema
|
172
|
+
AND TABLE_NAME = object
|
173
|
+
AND column_default LIKE 'nextval(%' || quote_ident(source_schema) || '%::regclass)'
|
174
|
+
LOOP
|
175
|
+
EXECUTE 'ALTER TABLE ' || buffer || ' ALTER COLUMN ' || column_ || ' SET DEFAULT ' || default_;
|
176
|
+
END LOOP;
|
177
|
+
|
178
|
+
END LOOP;
|
179
|
+
|
180
|
+
-- add FK constraint
|
181
|
+
FOR qry IN
|
182
|
+
SELECT 'ALTER TABLE ' || quote_ident(dest_schema) || '.' || quote_ident(rn.relname)
|
183
|
+
|| ' ADD CONSTRAINT ' || quote_ident(ct.conname) || ' ' || pg_get_constraintdef(ct.oid) || ';'
|
184
|
+
FROM pg_constraint ct
|
185
|
+
JOIN pg_class rn ON rn.oid = ct.conrelid
|
186
|
+
WHERE connamespace = src_oid
|
187
|
+
AND rn.relkind = 'r'
|
188
|
+
AND ct.contype = 'f'
|
189
|
+
|
190
|
+
LOOP
|
191
|
+
EXECUTE qry;
|
192
|
+
|
193
|
+
END LOOP;
|
194
|
+
|
195
|
+
|
196
|
+
-- Create views
|
197
|
+
FOR object IN
|
198
|
+
SELECT table_name::text,
|
199
|
+
view_definition
|
200
|
+
FROM information_schema.views
|
201
|
+
WHERE table_schema = quote_ident(source_schema)
|
202
|
+
|
203
|
+
LOOP
|
204
|
+
buffer := dest_schema || '.' || quote_ident(object);
|
205
|
+
SELECT view_definition INTO v_def
|
206
|
+
FROM information_schema.views
|
207
|
+
WHERE table_schema = quote_ident(source_schema)
|
208
|
+
AND table_name = quote_ident(object);
|
209
|
+
|
210
|
+
EXECUTE 'CREATE OR REPLACE VIEW ' || buffer || ' AS ' || v_def || ';' ;
|
211
|
+
|
212
|
+
END LOOP;
|
213
|
+
|
214
|
+
-- Create functions
|
215
|
+
FOR func_oid IN
|
216
|
+
SELECT oid
|
217
|
+
FROM pg_proc
|
218
|
+
WHERE pronamespace = src_oid
|
219
|
+
|
220
|
+
LOOP
|
221
|
+
SELECT pg_get_functiondef(func_oid) INTO qry;
|
222
|
+
SELECT replace(qry, source_schema, dest_schema) INTO dest_qry;
|
223
|
+
EXECUTE dest_qry;
|
224
|
+
|
225
|
+
END LOOP;
|
226
|
+
|
227
|
+
RETURN;
|
228
|
+
|
229
|
+
END;
|
230
|
+
|
231
|
+
$BODY$
|
232
|
+
LANGUAGE plpgsql VOLATILE
|
233
|
+
COST 100;
|
@@ -0,0 +1,70 @@
|
|
1
|
+
require 'cell/context'
|
2
|
+
require 'cell/model'
|
3
|
+
require 'cell/schema'
|
4
|
+
|
5
|
+
module Cell
|
6
|
+
class Configuration
|
7
|
+
attr_accessor :model
|
8
|
+
attr_accessor :identifier
|
9
|
+
attr_accessor :data_directory
|
10
|
+
attr_accessor :sidekiq
|
11
|
+
|
12
|
+
def initialize(model, &block)
|
13
|
+
@model = model
|
14
|
+
@identifier = :id
|
15
|
+
@data_directory = nil
|
16
|
+
@sidekiq = false
|
17
|
+
|
18
|
+
yield self
|
19
|
+
commit!(model)
|
20
|
+
end
|
21
|
+
|
22
|
+
def commit!(model)
|
23
|
+
fail "model class not specified" if model.nil? || ! model.is_a?(Class)
|
24
|
+
|
25
|
+
configure_model!(model)
|
26
|
+
configure_data_directory!(model)
|
27
|
+
configure_sidekiq!(model)
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def configure_model!(model)
|
33
|
+
->(identifier) do
|
34
|
+
model.define_singleton_method(:cell_id_column) do
|
35
|
+
identifier
|
36
|
+
end
|
37
|
+
end.call(self.identifier)
|
38
|
+
|
39
|
+
Cell.const_set(:Tenant, model)
|
40
|
+
model.prepend(::Cell::Model)
|
41
|
+
model.prepend(::Cell::Schema)
|
42
|
+
model.prepend(::Cell::Context)
|
43
|
+
end
|
44
|
+
|
45
|
+
def configure_data_directory!(model)
|
46
|
+
if self.data_directory
|
47
|
+
->(data_directory) do
|
48
|
+
model.define_singleton_method(:data_directory_root) do
|
49
|
+
data_directory
|
50
|
+
end
|
51
|
+
end.call(self.data_directory.to_s)
|
52
|
+
model.prepend(::Cell::DataDirectory)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def configure_sidekiq!(model)
|
57
|
+
if self.sidekiq
|
58
|
+
require 'cell/sidekiq'
|
59
|
+
::Cell::Sidekiq.configure!
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
private_constant :Configuration
|
64
|
+
|
65
|
+
module_function
|
66
|
+
def configure(model = nil, &block)
|
67
|
+
fail ArgumentError, "model or block required" if model.nil? && !block_given?
|
68
|
+
Configuration.new(model || block.binding.receiver, &block)
|
69
|
+
end
|
70
|
+
end
|
data/lib/cell/console.rb
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
module Cell
|
2
|
+
module Console
|
3
|
+
module_function
|
4
|
+
|
5
|
+
def default_console_tenant
|
6
|
+
return nil unless Rails.env.development? || (ENV['T'] && ENV['T'] != '')
|
7
|
+
Rails.application.eager_load!
|
8
|
+
|
9
|
+
if ENV['T']
|
10
|
+
Cell::Tenant.cell_find(ENV['T']).tap do |r|
|
11
|
+
fail "Couldn't locate tenant: #{ENV['T']}" if r.nil?
|
12
|
+
end
|
13
|
+
elsif Rails.env.development?
|
14
|
+
Cell::Tenant.first
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def configure!
|
19
|
+
if (t = default_console_tenant)
|
20
|
+
::Cell::Tenant.use(t)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
end
|
data/lib/cell/context.rb
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
# TenantContextManager allows switching the "active" tenant
|
2
|
+
require 'cell/with_schema'
|
3
|
+
|
4
|
+
module Cell
|
5
|
+
module Context
|
6
|
+
THREAD_KEY = :'Cell.cell_id'
|
7
|
+
|
8
|
+
module ClassMethods
|
9
|
+
def current
|
10
|
+
Thread.current[THREAD_KEY]
|
11
|
+
end
|
12
|
+
|
13
|
+
def use(tenant, exclusive: false)
|
14
|
+
unless tenant.nil? || tenant.is_a?(Cell::Tenant)
|
15
|
+
fail ArgumentError, "Invalid tenant: #{tenant.inspect}"
|
16
|
+
end
|
17
|
+
|
18
|
+
connection = Cell::Tenant.connection
|
19
|
+
|
20
|
+
if block_given?
|
21
|
+
saved_tenant = current
|
22
|
+
begin
|
23
|
+
::Cell::WithSchema.with_schema(tenant.schema_name,
|
24
|
+
exclusive: exclusive,
|
25
|
+
connection: connection) do
|
26
|
+
set_current(tenant)
|
27
|
+
yield Cell::Tenant.current
|
28
|
+
end
|
29
|
+
ensure
|
30
|
+
set_current(saved_tenant)
|
31
|
+
end
|
32
|
+
else
|
33
|
+
::Cell::WithSchema.with_schema(tenant.schema_name,
|
34
|
+
exclusive: exclusive,
|
35
|
+
connection: connection)
|
36
|
+
set_current(tenant)
|
37
|
+
tenant
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
def set_current(value)
|
43
|
+
Thread.current[THREAD_KEY] = value
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def use(*args, &block)
|
48
|
+
self.class.use(self, *args, &block)
|
49
|
+
end
|
50
|
+
|
51
|
+
def current?
|
52
|
+
self.class.current == self
|
53
|
+
end
|
54
|
+
|
55
|
+
def self.prepended(cls)
|
56
|
+
class << cls
|
57
|
+
prepend ClassMethods
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
|
3
|
+
module Cell
|
4
|
+
module DataDirectory
|
5
|
+
module ClassMethods
|
6
|
+
def data_directory_root
|
7
|
+
fail NotImplementedError
|
8
|
+
end
|
9
|
+
|
10
|
+
def data_directory_for_cell_id(cell_id)
|
11
|
+
cell_id = cell_id.to_s.gsub(/[^a-z0-9_]/i, '-')
|
12
|
+
File.join(data_directory_root, cell_id)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def data_directory
|
17
|
+
self.class.data_directory_for_cell_id(cell_id)
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
def create_data_directory!
|
22
|
+
FileUtils.mkdir_p(data_directory)
|
23
|
+
end
|
24
|
+
|
25
|
+
def update_data_directory!
|
26
|
+
src, dst = cell_id_change_set
|
27
|
+
FileUtils.mv(self.class.data_directory_for_cell_id(src), data_directory)
|
28
|
+
end
|
29
|
+
|
30
|
+
def destroy_data_directory!
|
31
|
+
FileUtils.rm_rf(data_directory)
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.prepended(cls)
|
35
|
+
cls.extend(ClassMethods)
|
36
|
+
|
37
|
+
cls.after_create_commit :create_data_directory!
|
38
|
+
cls.after_update_commit :update_data_directory!, if: :cell_id_changed?
|
39
|
+
cls.after_destroy_commit :destroy_data_directory!
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,114 @@
|
|
1
|
+
#
|
2
|
+
# Alright, here's how this works:
|
3
|
+
#
|
4
|
+
# A migration has two execution modes, global and targeted.
|
5
|
+
|
6
|
+
# "global" is ran with a "db:migrate", which means no Tenant is activated.
|
7
|
+
# "targeted" is ran once over each tenant, via "cell:db:migrate".
|
8
|
+
#
|
9
|
+
# In global mode:
|
10
|
+
# Each migration is ran twice: once with no explicit schema set, then again
|
11
|
+
# with "tenant_prototype" activated.
|
12
|
+
|
13
|
+
require 'cell/schema'
|
14
|
+
require 'cell/clone_schema'
|
15
|
+
require 'cell/with_schema'
|
16
|
+
|
17
|
+
module Cell
|
18
|
+
module Migration
|
19
|
+
def self.intercept(methods)
|
20
|
+
methods.each do |method|
|
21
|
+
define_method(method) do |*args, &block|
|
22
|
+
super(*args, &block) if execute_ddl?
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# This sucks.
|
28
|
+
intercept %i(add_belongs_to add_column add_foreign_key add_index
|
29
|
+
add_index_sort_order add_reference add_timestamps
|
30
|
+
change_column change_column_default change_column_null
|
31
|
+
change_table change_table_comment create_join_table
|
32
|
+
create_table drop_join_table drop_table
|
33
|
+
initialize_schema_migrations_table remove_belongs_to
|
34
|
+
remove_column remove_columns remove_foreign_key remove_index
|
35
|
+
remove_reference remove_timestamps rename_column
|
36
|
+
rename_column_indexes rename_index rename_table
|
37
|
+
rename_table_indexes table_alias_for table_comment) +
|
38
|
+
%i(enable_extension disable_extension truncate) +
|
39
|
+
%i(execute)
|
40
|
+
|
41
|
+
attr_accessor :pass_context
|
42
|
+
attr_accessor :global_block
|
43
|
+
|
44
|
+
def execute_ddl?
|
45
|
+
(pass_context == :global && global_block) ||
|
46
|
+
(pass_context == :prototype && !global_block) ||
|
47
|
+
(pass_context == :target && !global_block)
|
48
|
+
end
|
49
|
+
|
50
|
+
def global_schema
|
51
|
+
Cell::Schema.global_schema
|
52
|
+
end
|
53
|
+
|
54
|
+
def prototype_schema
|
55
|
+
Cell::Schema.prototype_schema
|
56
|
+
end
|
57
|
+
|
58
|
+
def tenant_schema
|
59
|
+
target.schema_name
|
60
|
+
end
|
61
|
+
|
62
|
+
def target
|
63
|
+
Cell::Tenant.current
|
64
|
+
end
|
65
|
+
|
66
|
+
def targeted?
|
67
|
+
!! target
|
68
|
+
end
|
69
|
+
|
70
|
+
def with_context(context, search_path)
|
71
|
+
Cell::WithSchema::with_schema(search_path, connection: connection) do
|
72
|
+
begin
|
73
|
+
saved_context = self.pass_context
|
74
|
+
self.pass_context = context
|
75
|
+
yield
|
76
|
+
ensure
|
77
|
+
self.pass_context = saved_context
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
|
83
|
+
def initialize_cell!
|
84
|
+
CloneSchema.install_function!(connection)
|
85
|
+
execute "CREATE SCHEMA #{connection.quote_schema_name(prototype_schema)}"
|
86
|
+
end
|
87
|
+
|
88
|
+
def exec_migration(con, direction)
|
89
|
+
if ! targeted?
|
90
|
+
with_context(:global, global_schema) do
|
91
|
+
super
|
92
|
+
end
|
93
|
+
|
94
|
+
with_context(:prototype, prototype_schema) do
|
95
|
+
super
|
96
|
+
end
|
97
|
+
else
|
98
|
+
with_context(:target, tenant_schema) do
|
99
|
+
super
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def global
|
105
|
+
saved = self.global_block
|
106
|
+
self.global_block = true
|
107
|
+
yield
|
108
|
+
ensure
|
109
|
+
self.global_block = saved
|
110
|
+
end
|
111
|
+
|
112
|
+
ActiveRecord::Migration.prepend(self)
|
113
|
+
end
|
114
|
+
end
|
data/lib/cell/model.rb
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
require 'cell/schema'
|
2
|
+
require 'cell/data_directory'
|
3
|
+
|
4
|
+
module Cell
|
5
|
+
module Model
|
6
|
+
class << self
|
7
|
+
attr_accessor :cls
|
8
|
+
end
|
9
|
+
|
10
|
+
module ClassMethods
|
11
|
+
def cell_id_column
|
12
|
+
fail NotImplementedError
|
13
|
+
end
|
14
|
+
|
15
|
+
def cell_find(cell_id)
|
16
|
+
find_by(cell_id_column => cell_id)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def cell_id
|
21
|
+
send(self.class.cell_id_column)
|
22
|
+
end
|
23
|
+
|
24
|
+
def cell_id_changed?
|
25
|
+
!! previous_changes[self.class.cell_id_column]
|
26
|
+
end
|
27
|
+
|
28
|
+
def cell_id_change_set
|
29
|
+
fail "cell_id was not changed" unless cell_id_changed?
|
30
|
+
previous_changes[self.class.cell_id_column]
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.prepended(cls)
|
34
|
+
cls.extend(ClassMethods)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
data/lib/cell/railtie.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
module Cell
|
2
|
+
class Railtie < Rails::Railtie
|
3
|
+
rake_tasks do
|
4
|
+
load 'cell/tasks/cell.rake'
|
5
|
+
end
|
6
|
+
|
7
|
+
console do
|
8
|
+
require 'cell/console'
|
9
|
+
Cell::Console.configure!
|
10
|
+
end
|
11
|
+
|
12
|
+
initializer "cell.sanity_check" do
|
13
|
+
require 'cell/sanity_check'
|
14
|
+
Cell::SanityCheck.sanity_check!
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
require 'active_record/connection_adapters/postgresql_adapter'
|
2
|
+
|
3
|
+
module Cell
|
4
|
+
module SanityCheck
|
5
|
+
|
6
|
+
module_function
|
7
|
+
def check_active_record_adapter!
|
8
|
+
pg_base_adapter = ::ActiveRecord::ConnectionAdapters::PostgreSQLAdapter
|
9
|
+
whitelist = ["PostgreSQLAdapter"]
|
10
|
+
adapter_name = ActiveRecord::Base.connection.adapter_name
|
11
|
+
|
12
|
+
unless ActiveRecord::Base.connection.is_a?(pg_base_adapter) ||
|
13
|
+
whitelist.include?(adapter_name)
|
14
|
+
msg <<~EOD
|
15
|
+
Cell uses PostgreSQL-specific features that cannot be represented
|
16
|
+
with your adapter. If your adapter is a PostgreSQL spin-off, please
|
17
|
+
open a pull request.
|
18
|
+
|
19
|
+
Whitelist: #{whitelist.inspect}
|
20
|
+
ActiveRecord adapter: #{adapter_name}
|
21
|
+
EOD
|
22
|
+
fail msg
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
|
27
|
+
def check_schema_format!
|
28
|
+
if Rails.application.config.active_record.schema_format != :sql
|
29
|
+
msg = <<~EOD
|
30
|
+
Cell uses PostgreSQL-specific features that cannot be represented
|
31
|
+
using a schema_format other than :sql. You need a definititive
|
32
|
+
structure.sql instead of a schema.rb. You can configure this by
|
33
|
+
adding the following line to your application.rb:
|
34
|
+
|
35
|
+
Rails.application.config.active_record.schema_format = :sql
|
36
|
+
EOD
|
37
|
+
fail msg
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def check_dump_schemas!
|
42
|
+
dump_schemas = Rails.application.config.active_record.dump_schemas
|
43
|
+
unless dump_schemas.to_s.match(/\bcell_prototype\b/)
|
44
|
+
msg = <<~EOD
|
45
|
+
Cell stores tenant templates in a PostgreSQL schema called
|
46
|
+
"cell_prototype".
|
47
|
+
|
48
|
+
Rails will not dump this schema by default with `db:structure:dump`
|
49
|
+
without explicitly setting `dump_schemas`.
|
50
|
+
|
51
|
+
You can configure this properly by adding a line like the following
|
52
|
+
in application.rb:
|
53
|
+
|
54
|
+
Rails.application.config.active_record.dump_schemas = "public,cell_prototype"
|
55
|
+
EOD
|
56
|
+
fail msg
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def sanity_check!
|
61
|
+
check_active_record_adapter!
|
62
|
+
check_schema_format!
|
63
|
+
check_dump_schemas!
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
data/lib/cell/schema.rb
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
require 'shellwords'
|
2
|
+
require 'cell/clone_schema'
|
3
|
+
require 'active_record/schema_migration'
|
4
|
+
|
5
|
+
module Cell
|
6
|
+
module Schema
|
7
|
+
SCHEMA_PREFIX = 't_'
|
8
|
+
MAX_SCHEMA_NAME_LENGTH = 63 # PostgreSQL baked-in default
|
9
|
+
MAX_CELL_ID_SIZE = MAX_SCHEMA_NAME_LENGTH - SCHEMA_PREFIX.size
|
10
|
+
|
11
|
+
module_function \
|
12
|
+
def global_schema
|
13
|
+
@global_schema ||= ActiveRecord::Base.connection.schema_search_path
|
14
|
+
end
|
15
|
+
|
16
|
+
module_function \
|
17
|
+
def prototype_schema
|
18
|
+
'cell_prototype'
|
19
|
+
end
|
20
|
+
|
21
|
+
module ClassMethods
|
22
|
+
def schema_name_for_cell_id(cell_id)
|
23
|
+
SCHEMA_PREFIX + cell_id.to_s.gsub(/[^a-z0-9_]/i, '-')
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def schema_name
|
28
|
+
self.class.schema_name_for_cell_id(cell_id)
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
def create_schema!
|
33
|
+
con = self.class.connection
|
34
|
+
con.transaction do
|
35
|
+
Cell::CloneSchema.clone_schema(con, Cell::Schema.prototype_schema,
|
36
|
+
schema_name)
|
37
|
+
Cell::CloneSchema.copy_schema_migrations_to(schema_name, connection: con)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def destroy_schema!
|
42
|
+
con = self.class.connection
|
43
|
+
con.execute \
|
44
|
+
"DROP SCHEMA #{con.quote_schema_name(schema_name)} CASCADE"
|
45
|
+
end
|
46
|
+
|
47
|
+
def update_schema!
|
48
|
+
src, dst = cell_id_change_set
|
49
|
+
src = self.class.schema_name_for_cell_id(src)
|
50
|
+
|
51
|
+
con = self.class.connection
|
52
|
+
con.execute \
|
53
|
+
"ALTER SCHEMA #{con.quote_schema_name(src)} RENAME " +
|
54
|
+
"TO #{con.quote_schema_name(schema_name)}"
|
55
|
+
end
|
56
|
+
|
57
|
+
def self.prepended(cls)
|
58
|
+
cls.extend(ClassMethods)
|
59
|
+
cls.after_create_commit :create_schema!
|
60
|
+
cls.after_update_commit :update_schema!, if: :cell_id_changed?
|
61
|
+
cls.after_destroy_commit :destroy_schema!
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
data/lib/cell/sidekiq.rb
ADDED
@@ -0,0 +1,58 @@
|
|
1
|
+
# This middleware passes along a tenant_id from client to server if a tenant
|
2
|
+
# is active when a job is spun off. The server process then activates the
|
3
|
+
# proper tenant if it's detected, so jobs are processed under the context of the
|
4
|
+
# same tenant that started the job on the client.
|
5
|
+
|
6
|
+
|
7
|
+
require 'sidekiq'
|
8
|
+
|
9
|
+
module Cell
|
10
|
+
module Sidekiq
|
11
|
+
KEY = 'Cell.cell_id'
|
12
|
+
|
13
|
+
class ClientMiddleware
|
14
|
+
def call(worker, msg, queue, redis_pool)
|
15
|
+
if (current_id = ::Cell.current&.cell_id)
|
16
|
+
msg[KEY] = current_id
|
17
|
+
end
|
18
|
+
|
19
|
+
yield
|
20
|
+
true
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
class ServerMiddleware
|
25
|
+
def call(worker, msg, queue, &block)
|
26
|
+
if msg.key?(KEY)
|
27
|
+
::Cell::Tenant.use(::Cell::Tenant.cell_find(msg[KEY]), &block)
|
28
|
+
else
|
29
|
+
::Cell::Tenant.use(nil, &block)
|
30
|
+
end
|
31
|
+
|
32
|
+
true
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
module_function
|
37
|
+
def configure!
|
38
|
+
::Sidekiq.configure_client do |config|
|
39
|
+
config.client_middleware do |chain|
|
40
|
+
chain.add ClientMiddleware
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
::Sidekiq.configure_server do |config|
|
45
|
+
# The server can also act as a client by creating more jobs, so we
|
46
|
+
# should configure the client inside the server block.
|
47
|
+
config.client_middleware do |chain|
|
48
|
+
chain.add ClientMiddleware
|
49
|
+
end
|
50
|
+
|
51
|
+
config.server_middleware do |chain|
|
52
|
+
chain.add ServerMiddleware
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
def db_tasks
|
2
|
+
Rake::Task.tasks.select do |t|
|
3
|
+
t.name.start_with?('db:')
|
4
|
+
end
|
5
|
+
end
|
6
|
+
|
7
|
+
|
8
|
+
def wrappable_task?(task)
|
9
|
+
fail ArgumentError unless task.name.start_with?('db:')
|
10
|
+
|
11
|
+
case task.name
|
12
|
+
when /^db:test\b/, /^db:structure\b/, /^db:schema\b/, /^db:setup\b/,
|
13
|
+
/^db:purge\b/, /^db:reset\b/, /^db:load_config\b/, /^db:create\b/,
|
14
|
+
/^db:drop\b/
|
15
|
+
false
|
16
|
+
else
|
17
|
+
true
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
|
22
|
+
def using_each_tenant(&block)
|
23
|
+
# We have to get the model that included Cell::Tenant
|
24
|
+
require Rails.root.join('config/environment')
|
25
|
+
|
26
|
+
tenants = if ENV['T']
|
27
|
+
ENV['T'].split(/\s*,\s*/).map do |tenant_id|
|
28
|
+
Cell::Tenant.cell_find(tenant_id).tap do |r|
|
29
|
+
fail "Unable to load tenant: #{tenant_id}" if r.nil?
|
30
|
+
end
|
31
|
+
end
|
32
|
+
else
|
33
|
+
::Cell::Tenant.respond_to?(:find_each) ? ::Cell::Tenant.find_each
|
34
|
+
: ::Cell::Tenant.each
|
35
|
+
end
|
36
|
+
|
37
|
+
tenants.each do |t|
|
38
|
+
Cell::Tenant.use(t, &block)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
|
43
|
+
db_tasks.each do |db_task|
|
44
|
+
if wrappable_task?(db_task)
|
45
|
+
if db_task.comment
|
46
|
+
desc "run `#{db_task.name}' for all tenants, or each in $T=t1,t2,..."
|
47
|
+
end
|
48
|
+
task "cell:#{db_task.name}" => ['environment', 'db:load_config'] do |t|
|
49
|
+
Rails.application.eager_load!
|
50
|
+
using_each_tenant do |tenant|
|
51
|
+
puts "Running #{db_task.name} for tenant '#{tenant.cell_id}'"
|
52
|
+
Rake::Task[db_task.name].execute
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
data/lib/cell/version.rb
CHANGED
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'active_record/base'
|
2
|
+
|
3
|
+
module Cell
|
4
|
+
module WithSchema
|
5
|
+
module_function
|
6
|
+
def with_schema(new_path, exclusive: false, connection:)
|
7
|
+
saved_path = connection.schema_search_path
|
8
|
+
new_path = connection.quote_schema_name(new_path)
|
9
|
+
active_path = exclusive ? new_path : "#{new_path},#{saved_path}"
|
10
|
+
|
11
|
+
set_schema_search_path(active_path, connection: connection)
|
12
|
+
|
13
|
+
if block_given?
|
14
|
+
begin
|
15
|
+
yield
|
16
|
+
ensure
|
17
|
+
set_schema_search_path(saved_path, connection: connection)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
private \
|
23
|
+
def set_schema_search_path(path, connection:)
|
24
|
+
if path != connection.schema_search_path
|
25
|
+
connection.schema_search_path = path
|
26
|
+
connection.clear_query_cache
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
data/lib/cell.rb
CHANGED
@@ -0,0 +1,25 @@
|
|
1
|
+
# TenantRoutingManager contains methods that can locate URLs and such for
|
2
|
+
# a given tenant.
|
3
|
+
|
4
|
+
module TenantRoutingManager
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
class_methods do
|
8
|
+
def slug_from_host(host)
|
9
|
+
if host && (match = host.match(/\A([a-z][a-z0-9-]*)\./))
|
10
|
+
return match[1]
|
11
|
+
end
|
12
|
+
return nil
|
13
|
+
end
|
14
|
+
|
15
|
+
def from_slug(slug)
|
16
|
+
return friendly.find(slug)
|
17
|
+
rescue ActiveRecord::RecordNotFound
|
18
|
+
return nil
|
19
|
+
end
|
20
|
+
|
21
|
+
def from_host(host)
|
22
|
+
from_slug(slug_from_host(host))
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module TenantUrlOptions
|
2
|
+
module_function \
|
3
|
+
def default_url_options
|
4
|
+
r = {}
|
5
|
+
r[:protocol] = 'https' if Rails.env.production?
|
6
|
+
r[:port] = 3000 unless Rails.env.production?
|
7
|
+
r[:host] = Tenant.current.try(:host)
|
8
|
+
r
|
9
|
+
end
|
10
|
+
|
11
|
+
end
|
12
|
+
|
13
|
+
class ActionMailer::Base
|
14
|
+
def default_url_options
|
15
|
+
TenantUrlOptions.default_url_options
|
16
|
+
end
|
17
|
+
end
|
metadata
CHANGED
@@ -1,15 +1,63 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: cell
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Mike Owens
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-
|
11
|
+
date: 2016-10-08 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rails
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 5.0.0
|
20
|
+
- - ">="
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: 5.0.0.1
|
23
|
+
type: :runtime
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
26
|
+
requirements:
|
27
|
+
- - "~>"
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: 5.0.0
|
30
|
+
- - ">="
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: 5.0.0.1
|
33
|
+
- !ruby/object:Gem::Dependency
|
34
|
+
name: pg
|
35
|
+
requirement: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - "~>"
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: 0.18.4
|
40
|
+
type: :runtime
|
41
|
+
prerelease: false
|
42
|
+
version_requirements: !ruby/object:Gem::Requirement
|
43
|
+
requirements:
|
44
|
+
- - "~>"
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: 0.18.4
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: minitest
|
49
|
+
requirement: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - "~>"
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: 5.9.0
|
54
|
+
type: :development
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - "~>"
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: 5.9.0
|
13
61
|
- !ruby/object:Gem::Dependency
|
14
62
|
name: bundler
|
15
63
|
requirement: !ruby/object:Gem::Requirement
|
@@ -52,9 +100,25 @@ files:
|
|
52
100
|
- Rakefile
|
53
101
|
- bin/console
|
54
102
|
- bin/setup
|
103
|
+
- bin/test
|
55
104
|
- cell.gemspec
|
56
105
|
- lib/cell.rb
|
106
|
+
- lib/cell/clone_schema.rb
|
107
|
+
- lib/cell/configure.rb
|
108
|
+
- lib/cell/console.rb
|
109
|
+
- lib/cell/context.rb
|
110
|
+
- lib/cell/data_directory.rb
|
111
|
+
- lib/cell/migration.rb
|
112
|
+
- lib/cell/model.rb
|
113
|
+
- lib/cell/railtie.rb
|
114
|
+
- lib/cell/sanity_check.rb
|
115
|
+
- lib/cell/schema.rb
|
116
|
+
- lib/cell/sidekiq.rb
|
117
|
+
- lib/cell/tasks/cell.rake
|
57
118
|
- lib/cell/version.rb
|
119
|
+
- lib/cell/with_schema.rb
|
120
|
+
- lib/dump/tenant_routing_manager.rb
|
121
|
+
- lib/dump/tenant_url_options.rb
|
58
122
|
homepage: https://github.com/metermd/cell
|
59
123
|
licenses:
|
60
124
|
- MIT
|