roomer 0.0.8
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.
- data/.autotest +17 -0
- data/.gitignore +9 -0
- data/.rvmrc +2 -0
- data/.travis.yml +2 -0
- data/Gemfile +31 -0
- data/MIT-LICENSE +20 -0
- data/README.md +116 -0
- data/Rakefile +16 -0
- data/TODO.md +10 -0
- data/doc/ActiveRecord/Base.html +328 -0
- data/doc/ApplicationController.html +116 -0
- data/doc/Roomer/Engine.html +116 -0
- data/doc/Roomer/Generators/InstallGenerator.html +419 -0
- data/doc/Roomer/Generators/SetupGenerator.html +209 -0
- data/doc/Roomer/Generators.html +108 -0
- data/doc/Roomer/Tools.html +702 -0
- data/doc/Roomer/VERSION.html +149 -0
- data/doc/Roomer.html +533 -0
- data/doc/RoomerCreate.html +121 -0
- data/doc/_index.html +232 -0
- data/doc/class_list.html +47 -0
- data/doc/css/common.css +1 -0
- data/doc/css/full_list.css +53 -0
- data/doc/css/style.css +320 -0
- data/doc/file.README.html +146 -0
- data/doc/file_list.html +49 -0
- data/doc/frames.html +13 -0
- data/doc/index.html +146 -0
- data/doc/js/app.js +205 -0
- data/doc/js/full_list.js +150 -0
- data/doc/js/jquery.js +16 -0
- data/doc/method_list.html +182 -0
- data/doc/top-level-namespace.html +105 -0
- data/lib/generators/roomer/install/install_generator.rb +39 -0
- data/lib/generators/roomer/install/templates/README +15 -0
- data/lib/generators/roomer/install/templates/roomer.rb +68 -0
- data/lib/generators/roomer/migration/migration_generator.rb +35 -0
- data/lib/generators/roomer/migration/templates/migration.rb +17 -0
- data/lib/generators/roomer/model/model_generator.rb +45 -0
- data/lib/generators/roomer/model/templates/migration.rb +16 -0
- data/lib/generators/roomer/setup/setup_generator.rb +38 -0
- data/lib/generators/roomer/setup/templates/global_migration.rb +20 -0
- data/lib/roomer/extensions/controller.rb +36 -0
- data/lib/roomer/extensions/model.rb +72 -0
- data/lib/roomer/helpers/generator_helper.rb +30 -0
- data/lib/roomer/helpers/migration_helper.rb +7 -0
- data/lib/roomer/helpers/postgres_helper.rb +75 -0
- data/lib/roomer/rails.rb +24 -0
- data/lib/roomer/schema.rb +63 -0
- data/lib/roomer/schema_dumper.rb +36 -0
- data/lib/roomer/tasks/migrate.rake +85 -0
- data/lib/roomer/utils.rb +68 -0
- data/lib/roomer/version.rb +14 -0
- data/lib/roomer.rb +125 -0
- data/roomer.gemspec +22 -0
- data/test/config.rb +3 -0
- data/test/config.yml +7 -0
- data/test/generators/install_generator_test.rb +13 -0
- data/test/generators/migration_generator_test.rb +33 -0
- data/test/generators/model_generator_test.rb +25 -0
- data/test/generators/setup_generator_test.rb +25 -0
- data/test/rails_app/app/controllers/application_controller.rb +3 -0
- data/test/rails_app/app/helpers/application_helper.rb +2 -0
- data/test/rails_app/app/models/tenant.rb +12 -0
- data/test/rails_app/app/views/layouts/application.html.erb +14 -0
- data/test/rails_app/config/application.rb +50 -0
- data/test/rails_app/config/boot.rb +6 -0
- data/test/rails_app/config/database.yml +2 -0
- data/test/rails_app/config/environment.rb +5 -0
- data/test/rails_app/config/environments/development.rb +26 -0
- data/test/rails_app/config/environments/production.rb +49 -0
- data/test/rails_app/config/environments/test.rb +35 -0
- data/test/rails_app/config/initializers/backtrace_silencers.rb +7 -0
- data/test/rails_app/config/initializers/inflections.rb +10 -0
- data/test/rails_app/config/initializers/mime_types.rb +5 -0
- data/test/rails_app/config/initializers/roomer.rb +68 -0
- data/test/rails_app/config/initializers/secret_token.rb +7 -0
- data/test/rails_app/config/initializers/session_store.rb +8 -0
- data/test/rails_app/config/locales/en.yml +5 -0
- data/test/rails_app/config/routes.rb +58 -0
- data/test/rails_app/config.ru +4 -0
- data/test/rails_app/db/migrate/global/20110825231409_roomer_create_tenants.rb +20 -0
- data/test/rails_app/db/seeds.rb +7 -0
- data/test/rails_app/lib/tasks/.gitkeep +0 -0
- data/test/rails_app/public/404.html +26 -0
- data/test/rails_app/public/422.html +26 -0
- data/test/rails_app/public/500.html +26 -0
- data/test/rails_app/public/favicon.ico +0 -0
- data/test/rails_app/public/images/rails.png +0 -0
- data/test/rails_app/public/index.html +239 -0
- data/test/rails_app/public/javascripts/application.js +2 -0
- data/test/rails_app/public/javascripts/controls.js +965 -0
- data/test/rails_app/public/javascripts/dragdrop.js +974 -0
- data/test/rails_app/public/javascripts/effects.js +1123 -0
- data/test/rails_app/public/javascripts/prototype.js +6001 -0
- data/test/rails_app/public/javascripts/rails.js +191 -0
- data/test/rails_app/public/robots.txt +5 -0
- data/test/rails_app/public/stylesheets/.gitkeep +0 -0
- data/test/roomer/helpers/postgres_helper_test.rb +62 -0
- data/test/roomer/roomer_test.rb +37 -0
- data/test/test_helper.rb +23 -0
- metadata +228 -0
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
module Roomer
|
|
2
|
+
module Extensions
|
|
3
|
+
module Model
|
|
4
|
+
def self.included(base)
|
|
5
|
+
base.extend(ClassMethods)
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
module ClassMethods
|
|
9
|
+
# Sets the roomer scope for the model and changes the model's table_name_prefix
|
|
10
|
+
# Sets the table name prefix (schema name) to current_tenant's
|
|
11
|
+
# If :shared is passed, the global schema will be used as the table name prefix
|
|
12
|
+
# if :tenanted is pased, the current tenant's schema will be used as the table name prefix
|
|
13
|
+
# @return [Symbol] :shared or :tenanted
|
|
14
|
+
def roomer(scope)
|
|
15
|
+
case scope
|
|
16
|
+
when :shared
|
|
17
|
+
@roomer_scope = :shared
|
|
18
|
+
when :tenanted
|
|
19
|
+
@roomer_scope = :tenanted
|
|
20
|
+
else
|
|
21
|
+
raise "Invalid roomer model scope. Choose :shared or :tenanted"
|
|
22
|
+
end
|
|
23
|
+
roomer_set_table_name_prefix
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Confirms if model is shared
|
|
27
|
+
# @return [True,False]
|
|
28
|
+
def shared?
|
|
29
|
+
@roomer_scope == :shared
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Confirms if model is scoped by tenant
|
|
33
|
+
# @return [True,False]
|
|
34
|
+
def tenanted?
|
|
35
|
+
@roomer_scope == :tenanted
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Resets model's cached table_name and
|
|
39
|
+
# column information.
|
|
40
|
+
# This method needs to get called whenever
|
|
41
|
+
# the current tenant changes
|
|
42
|
+
def roomer_reset
|
|
43
|
+
roomer_set_table_name_prefix
|
|
44
|
+
reset_table_name
|
|
45
|
+
reset_column_information
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
protected
|
|
49
|
+
# Resolves the full table name prefix
|
|
50
|
+
def roomer_full_table_name_prefix(schema_name)
|
|
51
|
+
"#{schema_name.to_s}#{Roomer.schema_seperator}"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Sets the model's table name prefix to the current tenant's schema name
|
|
55
|
+
# Defaults to public if model is marked as tenanted but tenant table
|
|
56
|
+
# hasn't been populated
|
|
57
|
+
def roomer_set_table_name_prefix
|
|
58
|
+
self.table_name_prefix = begin
|
|
59
|
+
case @roomer_scope
|
|
60
|
+
when :shared
|
|
61
|
+
roomer_full_table_name_prefix(Roomer.shared_schema_name)
|
|
62
|
+
when :tenanted
|
|
63
|
+
roomer_full_table_name_prefix(Roomer.current_tenant.try(Roomer.tenant_schema_name_column))
|
|
64
|
+
else
|
|
65
|
+
""
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
module Roomer
|
|
2
|
+
module Helpers
|
|
3
|
+
module GeneratorHelper
|
|
4
|
+
|
|
5
|
+
# Check to see if the model file exists, should be used in a Generator
|
|
6
|
+
# @return [True,False]
|
|
7
|
+
def model_exists?
|
|
8
|
+
File.exists?(File.join(destination_root, model_path))
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# Returns the path of the model
|
|
12
|
+
# @return [String] model_path string representing the model location
|
|
13
|
+
def model_path
|
|
14
|
+
@model_path ||= File.join("app", "models", "#{file_path}.rb")
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Reads the --shared option specified when running "rails generate roomer:model"
|
|
18
|
+
# @return [True,False]
|
|
19
|
+
def shared?
|
|
20
|
+
@shared ||= options[:shared]
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Fetchs the migration directory for the migrations
|
|
24
|
+
def migration_dir
|
|
25
|
+
return Roomer.shared_migrations_directory if shared?
|
|
26
|
+
return Roomer.tenanted_migrations_directory
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
module Roomer
|
|
2
|
+
module Helpers
|
|
3
|
+
module PostgresHelper
|
|
4
|
+
|
|
5
|
+
# Creates a schema in PostgreSQL
|
|
6
|
+
# @param [#to_s] schema_name declaring the name of the schema
|
|
7
|
+
def create_schema(schema_name)
|
|
8
|
+
ActiveRecord::Base.connection.execute "CREATE SCHEMA \"#{schema_name.to_s}\""
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# Drops a schema and all it's objects (Cascades)
|
|
12
|
+
# @param [#to_s] schema_name declaring the name of the schema to drop
|
|
13
|
+
def drop_schema(schema_name)
|
|
14
|
+
ActiveRecord::Base.connection.execute("DROP SCHEMA IF EXISTS \"#{schema_name.to_s}\" CASCADE")
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# lists the schemas available
|
|
18
|
+
# @return [Array] list of schemas
|
|
19
|
+
def schemas
|
|
20
|
+
ActiveRecord::Base.connection.query("SELECT nspname FROM pg_namespace WHERE nspname !~ '^pg_.*'").flatten.map(&:to_s)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Ensures the schema and schema_migrations exist(creates them otherwise)
|
|
24
|
+
# and executes the code block
|
|
25
|
+
# @param [#to_s] schema_name declaring name to ensure
|
|
26
|
+
# @param [#call] &block code to execute
|
|
27
|
+
#
|
|
28
|
+
# Example:
|
|
29
|
+
#
|
|
30
|
+
# ensuring_schema(:global) do
|
|
31
|
+
# ActiveRecord::Migrator.migrate('/db/migrate', '20110812012536')
|
|
32
|
+
# end
|
|
33
|
+
def ensuring_schema(schema_name, &block)
|
|
34
|
+
raise ArgumentError.new("schema_name not present") unless schema_name
|
|
35
|
+
ActiveRecord::Migration.verbose = ENV["VERBOSE"] ? ENV["VERBOSE"] == "true" : true
|
|
36
|
+
create_schema(schema_name) unless schemas.include?(schema_name.to_s)
|
|
37
|
+
ensure_prefix(schema_name) do
|
|
38
|
+
ensure_schema_migrations
|
|
39
|
+
yield
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Ensures the same ActiveRecord::Base#table_name_prefix for all the
|
|
44
|
+
# models executed in the block
|
|
45
|
+
# @param [#to_s] A Symbol declaring the table name prefix
|
|
46
|
+
# @param [#call] code to execute
|
|
47
|
+
# @note All the Models will have the same prefix, caution is advised
|
|
48
|
+
#
|
|
49
|
+
# Example:
|
|
50
|
+
#
|
|
51
|
+
# ensure_prefix(:global) do
|
|
52
|
+
# Person.find(1) # => will execute "SELECT id FROM 'global.person' where 'id' = 1"
|
|
53
|
+
# end
|
|
54
|
+
def ensure_prefix(prefix, &block)
|
|
55
|
+
@old_prefix = ActiveRecord::Base.table_name_prefix
|
|
56
|
+
ActiveRecord::Base.table_name_prefix = "#{prefix.to_s}#{Roomer.schema_seperator.to_s}"
|
|
57
|
+
yield
|
|
58
|
+
ActiveRecord::Base.table_name_prefix = @old_prefix
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Ensures schema_migrations table exists and creates otherwise
|
|
62
|
+
# @see ActiveRecord::Base.connection#initialize_schema_migrations_table
|
|
63
|
+
def ensure_schema_migrations
|
|
64
|
+
ActiveRecord::Base.connection.initialize_schema_migrations_table
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Determine if there are any pending migrations in the shared migrations directory
|
|
68
|
+
# @returns true if migrations are pending
|
|
69
|
+
def shared_migrations_pending?
|
|
70
|
+
ActiveRecord::Migrator.new(:up,Roomer.shared_migrations_directory)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
data/lib/roomer/rails.rb
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
module Roomer
|
|
2
|
+
class RoomerEngine < ::Rails::Engine
|
|
3
|
+
|
|
4
|
+
initializer 'roomer.extensions' do |app|
|
|
5
|
+
# load model extensions
|
|
6
|
+
ActiveSupport.on_load(:active_record) do
|
|
7
|
+
include Roomer::Extensions::Model
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
# load controller extensions
|
|
11
|
+
ActiveSupport.on_load(:action_controller) do
|
|
12
|
+
include Roomer::Extensions::Controller
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# load all the rake tasks
|
|
17
|
+
rake_tasks do
|
|
18
|
+
Dir[File.expand_path("../tasks/*.rake", __FILE__)].each do |file|
|
|
19
|
+
load file
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
require 'active_support/core_ext/object/blank'
|
|
2
|
+
|
|
3
|
+
module Roomer
|
|
4
|
+
# Roomer::Schema extends ActiveRecord::Schema
|
|
5
|
+
# Roomer::Schema is currently only supported by the Postgres database adapter
|
|
6
|
+
|
|
7
|
+
class Schema < ActiveRecord::Migration
|
|
8
|
+
extend Roomer::Helpers::PostgresHelper
|
|
9
|
+
|
|
10
|
+
def self.migrations_path(scope=:tenanted)
|
|
11
|
+
case scope
|
|
12
|
+
when :shared
|
|
13
|
+
return Roomer.shared_migrations_directory
|
|
14
|
+
when :tenanted
|
|
15
|
+
return Roomer.tenanted_migrations_directory
|
|
16
|
+
else
|
|
17
|
+
raise 'Invalid scope parameter'
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def self.dump(scope=:tenanted)
|
|
22
|
+
case scope
|
|
23
|
+
when :shared
|
|
24
|
+
schema_name = Roomer.shared_schema_name.to_s
|
|
25
|
+
filename = Roomer.shared_schema_filename
|
|
26
|
+
when :tenanted
|
|
27
|
+
tenant = Tenant.first
|
|
28
|
+
return if tenant.blank?
|
|
29
|
+
|
|
30
|
+
schema_name = Tenant.first.schema_name.to_s
|
|
31
|
+
filename = Roomer.tenanted_schema_filename
|
|
32
|
+
end
|
|
33
|
+
FileUtils.mkdir_p(Roomer.schemas_directory) unless File.exists?(Roomer.schemas_directory)
|
|
34
|
+
filepath = File.expand_path(File.join(Roomer.schemas_directory, filename))
|
|
35
|
+
ActiveRecord::Base.connection.schema_search_path = schema_name
|
|
36
|
+
ActiveRecord::Base.table_name_prefix = "#{schema_name}."
|
|
37
|
+
Roomer::SchemaDumper.dump(ActiveRecord::Base.connection, File.new(filepath, "w"))
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def self.load(schema_name, scope=:tenanted)
|
|
41
|
+
filename = begin
|
|
42
|
+
if scope == :shared
|
|
43
|
+
Roomer.shared_schema_filename
|
|
44
|
+
elsif scope == :tenanted
|
|
45
|
+
Roomer.tenanted_schema_filename
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
filepath = File.expand_path(File.join(Roomer.schemas_directory, filename))
|
|
49
|
+
return unless File.exists?(filepath)
|
|
50
|
+
|
|
51
|
+
ensuring_schema(schema_name) do
|
|
52
|
+
Object.load(filepath)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def self.define(info={}, &block)
|
|
57
|
+
instance_eval(&block)
|
|
58
|
+
unless info[:version].blank?
|
|
59
|
+
ActiveRecord::Base.connection.assume_migrated_upto_version(info[:version], migrations_path)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
module Roomer
|
|
2
|
+
class SchemaDumper < ActiveRecord::SchemaDumper
|
|
3
|
+
|
|
4
|
+
protected
|
|
5
|
+
def header(stream)
|
|
6
|
+
define_params = @version ? ":version => #{@version}" : ""
|
|
7
|
+
stream.puts <<HEADER
|
|
8
|
+
# It's strongly recommended to check this file into your version control system.
|
|
9
|
+
|
|
10
|
+
Roomer::Schema.define(#{define_params}) do
|
|
11
|
+
|
|
12
|
+
HEADER
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def indexes(table, stream)
|
|
16
|
+
if (indexes = @connection.indexes(table)).any?
|
|
17
|
+
add_index_statements = indexes.map do |index|
|
|
18
|
+
statement_parts = [ ('add_index ' + index.table.inspect) ]
|
|
19
|
+
statement_parts << index.columns.inspect
|
|
20
|
+
statement_parts << (':name => "' + index.columns.first.to_s.split('.').last + '"')
|
|
21
|
+
statement_parts << ':unique => true' if index.unique
|
|
22
|
+
|
|
23
|
+
index_lengths = index.lengths.compact if index.lengths.is_a?(Array)
|
|
24
|
+
if index_lengths.present?
|
|
25
|
+
statement_parts << (':length => ' + Hash[*index.columns.zip(index.lengths).flatten].inspect)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
' ' + statement_parts.join(', ')
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
stream.puts add_index_statements.sort.join("\n")
|
|
32
|
+
stream.puts
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
include Roomer::Helpers::PostgresHelper
|
|
2
|
+
include Roomer::Helpers::GeneratorHelper
|
|
3
|
+
|
|
4
|
+
namespace :roomer do
|
|
5
|
+
namespace :shared do
|
|
6
|
+
desc "Migrates the shared tables. Target specific version with VERSION=x"
|
|
7
|
+
task :migrate => :environment do
|
|
8
|
+
version = ENV["VERSION"] ? ENV["VERSION"].to_i : nil
|
|
9
|
+
ensuring_schema(Roomer.shared_schema_name) do
|
|
10
|
+
ActiveRecord::Migrator.migrate(Roomer.shared_migrations_directory, version)
|
|
11
|
+
end
|
|
12
|
+
Roomer::Schema.dump(:shared)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
desc "Rolls back shared tables. Target specific version with STEP=x"
|
|
16
|
+
task :rollback => :environment do
|
|
17
|
+
step = ENV['STEP'] ? ENV['STEP'].to_i : 1
|
|
18
|
+
ensuring_schema(Roomer.shared_schema_name) do
|
|
19
|
+
ActiveRecord::Migrator.rollback(Roomer.shared_migrations_directory, step)
|
|
20
|
+
end
|
|
21
|
+
Roomer::Schema.dump(:shared)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
namespace :schema do
|
|
25
|
+
desc "Load shared schema into database"
|
|
26
|
+
task :load => :environment do
|
|
27
|
+
Roomer::Schema.load(Roomer.shared_schema_name, :shared)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
desc "Dumps the shared schema to shared/schema.rb. This file can be portably used against any DB supported by AR"
|
|
31
|
+
task :dump => :environment do
|
|
32
|
+
Roomer::Schema.dump(:shared)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
namespace :tenanted do
|
|
38
|
+
desc "Migrates the tenanted tables. Target specific version with VERSION=x"
|
|
39
|
+
task :migrate => :environment do
|
|
40
|
+
version = ENV["VERSION"] ? ENV["VERSION"].to_i : nil
|
|
41
|
+
Roomer.tenant_model.find(:all).each do |tenant|
|
|
42
|
+
ensuring_schema(tenant.try(Roomer.tenant_schema_name_column)) do
|
|
43
|
+
ActiveRecord::Migrator.migrate(Roomer.tenanted_migrations_directory, version)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
Roomer::Schema.dump(:tenanted)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
desc "Rolls back tenanted tables. Target specific version with STEP=x"
|
|
50
|
+
task :rollback => :environment do
|
|
51
|
+
step = ENV['STEP'] ? ENV['STEP'].to_i : 1
|
|
52
|
+
Roomer.tenant_model.find(:all).each do |tenant|
|
|
53
|
+
ensuring_schema(tenant.try(Roomer.tenant_schema_name_column)) do
|
|
54
|
+
ActiveRecord::Migrator.rollback(Roomer.tenanted_migrations_directory, step)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
Roomer::Schema.dump(:tenanted)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
namespace :schema do
|
|
61
|
+
desc "Load tenanted schema into database"
|
|
62
|
+
task :load => :environment do
|
|
63
|
+
schema_name = ENV['SCHEMA_NAME']
|
|
64
|
+
raise "No schema name provided. Try: env SCHEMA_NAME=" if schema_name.blank?
|
|
65
|
+
Roomer::Schema.load(schema_name)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
desc "Dumps tenanted schema file to migrate/schema.rb. This file can be portably used against any DB supported by AR"
|
|
69
|
+
task :dump => :environment do
|
|
70
|
+
Roomer::Schema.dump
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
desc "Runs shared and tenanted migrations"
|
|
76
|
+
task :migrate do
|
|
77
|
+
if ENV["VERSION"].blank?
|
|
78
|
+
Rake::Task["roomer:shared:migrate"].invoke
|
|
79
|
+
Rake::Task["roomer:tenanted:migrate"].invoke
|
|
80
|
+
else
|
|
81
|
+
Rake::Task["roomer:tenanted:migrate"].invoke
|
|
82
|
+
Rake::Task["roomer:shared:migrate"].invoke
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
data/lib/roomer/utils.rb
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
module Roomer
|
|
2
|
+
module Utils
|
|
3
|
+
|
|
4
|
+
# Rails DB Migrations Directory
|
|
5
|
+
# @return [String] full path to the current migrations directory
|
|
6
|
+
def migrations_directory
|
|
7
|
+
ActiveRecord::Migrator.migrations_path
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
# Directory where the models are stored
|
|
11
|
+
# @return [String] path of the directory where the models are stored
|
|
12
|
+
def model_directory
|
|
13
|
+
File.join("app","models")
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Consutructs the full name for the tenants table with schema
|
|
17
|
+
# Example: 'global.tenant'
|
|
18
|
+
# @return [String] full name of the tenant table
|
|
19
|
+
def full_tenants_table_name
|
|
20
|
+
"#{shared_schema_name}#{schema_seperator}#{tenants_table}"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Constructs the full path to the shared schema directory
|
|
24
|
+
# Example: /Users/Greg/Projects/roomer/db/migrate/global
|
|
25
|
+
# @return [String] full path to the shared schema directory
|
|
26
|
+
def full_shared_schema_migration_path
|
|
27
|
+
"#{Rails.root}/#{shared_migrations_directory}"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Returns tenant model as a constant
|
|
31
|
+
# Example: Tenant
|
|
32
|
+
# @return Object
|
|
33
|
+
def tenant_model
|
|
34
|
+
Roomer.tenants_table.to_s.classify.constantize
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Sets current tenant from ApplicationController into a Thread
|
|
38
|
+
# local variable. Works only with thread-safe Rails as long as
|
|
39
|
+
# it gets set on every request
|
|
40
|
+
# @return [Symbol] the current tenant key in the thread
|
|
41
|
+
def current_tenant=(val)
|
|
42
|
+
key = :"roomer_current_tenant"
|
|
43
|
+
Thread.current[key] = val
|
|
44
|
+
ensure_tenant_model_reset
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Fetches the current tenant
|
|
48
|
+
# @return [Symbol] the current tenant key in the thread
|
|
49
|
+
def current_tenant
|
|
50
|
+
key = :"roomer_current_tenant"
|
|
51
|
+
Thread.current[key]
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Reset current tenant
|
|
55
|
+
# @return [Nil]
|
|
56
|
+
def reset_current_tenant
|
|
57
|
+
key = :"roomer_current_tenant"
|
|
58
|
+
Thread.current[key] = nil
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Reset cached data in tenanted models
|
|
62
|
+
def ensure_tenant_model_reset
|
|
63
|
+
ActiveRecord::Base.descendants.each do |model|
|
|
64
|
+
model.roomer_reset if model.tenanted?
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
data/lib/roomer.rb
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
require 'rails'
|
|
2
|
+
require 'active_support'
|
|
3
|
+
require 'active_support/dependencies'
|
|
4
|
+
require 'active_record'
|
|
5
|
+
require 'roomer/version'
|
|
6
|
+
|
|
7
|
+
module ActiveRecord
|
|
8
|
+
autoload :Migration, 'active_record/migration'
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
module Roomer
|
|
12
|
+
autoload :Utils, 'roomer/utils'
|
|
13
|
+
|
|
14
|
+
module Helpers
|
|
15
|
+
autoload :GeneratorHelper, 'roomer/helpers/generator_helper'
|
|
16
|
+
autoload :PostgresHelper, 'roomer/helpers/postgres_helper'
|
|
17
|
+
autoload :MigrationHelper, 'roomer/helpers/migration_helper'
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
module Extensions
|
|
21
|
+
autoload :Model, 'roomer/extensions/model'
|
|
22
|
+
autoload :Controller, 'roomer/extensions/controller'
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
autoload :SchemaDumper, 'roomer/schema_dumper'
|
|
26
|
+
autoload :Schema, 'roomer/schema'
|
|
27
|
+
|
|
28
|
+
extend Utils
|
|
29
|
+
|
|
30
|
+
# The URL routing strategy. Roomer currently supports two routing strategies (:domain and :path)
|
|
31
|
+
# * :domain - Using domain name to identify the tenant. This could include a subdomain
|
|
32
|
+
# * :path - identifying the tenant by the path
|
|
33
|
+
# Example:
|
|
34
|
+
# Domain
|
|
35
|
+
# ------
|
|
36
|
+
# http://mytenant.myapp.com - If you tenant has a subdomain under your domain
|
|
37
|
+
# http://mytenant.com - If the tenant choose to use their own top level domain name
|
|
38
|
+
# http://myapp.mytenant.com If the tenant chooses to use their own subdomain under thier TLD
|
|
39
|
+
# Path
|
|
40
|
+
# ----
|
|
41
|
+
# http://yourapp.com/tenant
|
|
42
|
+
mattr_accessor :url_routing_strategy
|
|
43
|
+
@@url_routing_strategy = :domain
|
|
44
|
+
|
|
45
|
+
# name of the shared schema where all the shared tables are be present
|
|
46
|
+
mattr_accessor :shared_schema_name
|
|
47
|
+
@@shared_schema_name = :global
|
|
48
|
+
|
|
49
|
+
# The name of the table where the tenants are stored, this table must
|
|
50
|
+
# contain two required columns configured under tenant_url_identifier_column and
|
|
51
|
+
# tenant_schema_name_column
|
|
52
|
+
mattr_accessor :tenants_table
|
|
53
|
+
@@tenants_table = :tenants
|
|
54
|
+
|
|
55
|
+
# The column name that stores the url identfier in the tenants tables.
|
|
56
|
+
# A url idenfier is a unique value that identifies the tenant from the URL.
|
|
57
|
+
# For e.g: if you use domains and the url is http://mytenant.myapp.com,
|
|
58
|
+
# the url identifier would "mytenant"
|
|
59
|
+
mattr_accessor :tenant_url_identifier_column
|
|
60
|
+
@@tenant_url_identifier_column = :url_identifier
|
|
61
|
+
|
|
62
|
+
# The column name that strores the schema name in the tenants tables.
|
|
63
|
+
mattr_accessor :tenant_schema_name_column
|
|
64
|
+
@@tenant_schema_name_column = :schema_name
|
|
65
|
+
|
|
66
|
+
# The schema seperator. This is used when generating the table name.
|
|
67
|
+
# The default is set to "."
|
|
68
|
+
# Example: tenant's table by default will be global.tenants
|
|
69
|
+
mattr_accessor :schema_seperator
|
|
70
|
+
@@schema_seperator = '.'
|
|
71
|
+
|
|
72
|
+
# Directory where schema files will be stored.
|
|
73
|
+
mattr_accessor :schemas_directory
|
|
74
|
+
@@schemas_directory = File.expand_path(File.join("db", "schemas"))
|
|
75
|
+
|
|
76
|
+
# Tenanted schema filename.
|
|
77
|
+
mattr_accessor :tenanted_schema_filename
|
|
78
|
+
@@tenanted_schema_filename = "tenanted_schema.rb"
|
|
79
|
+
|
|
80
|
+
# Shared schema filename.
|
|
81
|
+
mattr_accessor :shared_schema_filename
|
|
82
|
+
@@shared_schema_filename = "shared_schema.rb"
|
|
83
|
+
|
|
84
|
+
# Use Tentant migrations directory?
|
|
85
|
+
# Default is set to false
|
|
86
|
+
mattr_accessor :use_tenanted_migrations_directory
|
|
87
|
+
alias_method :use_tenanted_migrations_directory?,
|
|
88
|
+
:use_tenanted_migrations_directory
|
|
89
|
+
@@use_tenanted_migrations_directory = false
|
|
90
|
+
|
|
91
|
+
# Directory where the tenanted migrations are stored.
|
|
92
|
+
# This will be used only if use_tenanted_migration_directory is set to
|
|
93
|
+
# true if not usual rails migraiton directory db/migrate will be used
|
|
94
|
+
mattr_writer :tenanted_migrations_directory
|
|
95
|
+
@@tenanted_migrations_directory = File.join(migrations_directory,tenants_table.to_s)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
# Directory where shared migrations are stored.
|
|
99
|
+
mattr_accessor :shared_migrations_directory
|
|
100
|
+
@@shared_migrations_directory = File.join(migrations_directory,shared_schema_name.to_s)
|
|
101
|
+
|
|
102
|
+
# Fetches the migrations directory for Tenanted migrations.
|
|
103
|
+
# returns the standard rails migration directory "db/migrate" is the
|
|
104
|
+
# use_tenanted_migrations_directory is set to false
|
|
105
|
+
# @return [String] String representing the tenanted migrations
|
|
106
|
+
def self.tenanted_migrations_directory
|
|
107
|
+
if self.use_tenanted_migrations_directory
|
|
108
|
+
return @@tenanted_migrations_directory
|
|
109
|
+
end
|
|
110
|
+
return migrations_directory
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Default way to setup Roomer. Run rails generate roomer:install to create
|
|
114
|
+
# a fresh initializer with all configuration values.
|
|
115
|
+
# @example
|
|
116
|
+
# Roomer.setup do |config|
|
|
117
|
+
# config.url_routing_strategy = :domain
|
|
118
|
+
# end
|
|
119
|
+
def self.setup
|
|
120
|
+
yield self
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
require 'roomer/rails'
|
data/roomer.gemspec
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
|
3
|
+
require "roomer/version"
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |s|
|
|
6
|
+
s.name = "roomer"
|
|
7
|
+
s.version = Roomer::VERSION::STRING
|
|
8
|
+
s.platform = Gem::Platform::RUBY
|
|
9
|
+
s.homepage = "https://github.com/gosuri/roomer"
|
|
10
|
+
s.summary = "Roomer is a multitenant framework for Rails using PostgreSQL"
|
|
11
|
+
s.description = Roomer::VERSION::SUMMARY
|
|
12
|
+
|
|
13
|
+
s.authors = ["Greg Osuri","Daniel Ceballos"]
|
|
14
|
+
s.email = ["gosuri@gmail.com","dceballos@gmail.com"]
|
|
15
|
+
|
|
16
|
+
s.files = `git ls-files`.split("\n")
|
|
17
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
|
18
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
|
19
|
+
s.require_paths = ["lib"]
|
|
20
|
+
|
|
21
|
+
s.add_dependency 'rails','~> 3.0.9'
|
|
22
|
+
end
|
data/test/config.rb
ADDED
data/test/config.yml
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
require "test_helper"
|
|
2
|
+
require "generators/roomer/install/install_generator"
|
|
3
|
+
|
|
4
|
+
class InstallGeneratorTest < Rails::Generators::TestCase
|
|
5
|
+
tests Roomer::Generators::InstallGenerator
|
|
6
|
+
destination File.expand_path("../../tmp", __FILE__)
|
|
7
|
+
setup :prepare_destination
|
|
8
|
+
|
|
9
|
+
test "assert initializer file created" do
|
|
10
|
+
run_generator
|
|
11
|
+
assert_file "config/initializers/roomer.rb"
|
|
12
|
+
end
|
|
13
|
+
end
|