cell 0.1.0 → 0.1.1
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 +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
|