multi_tenant_subdomain 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0e0dbab0670afbd0e9ec60de68b40fb3021a6f23c489e7065d64200194d5051e
4
- data.tar.gz: e1aa495aabba98159e312e16364398627b199bb8197a5e734e62e867c92dfce7
3
+ metadata.gz: 17f95fbb6c386d5bcb5e6f3d0f118edede54214733aba63b8846e863aaeea99a
4
+ data.tar.gz: 5058ee214f80b0af3c21b036f86a1d990b8b1292400703ade74625766d961a6a
5
5
  SHA512:
6
- metadata.gz: 431d99f799cc6870052617bb287a58f8c1c935bfb46c846bda3398b32971e4fb6990fb7feb3bedc9f1cc1ed576cb58da143fd0050d2f22101242fb1d92c6718e
7
- data.tar.gz: e8eabe2cb7de0ea0e4cdd609027fbcca5acac8f3416367cabbfb0683e5a5b253ffd0c2f48d5b5509cf1397eb02fd15445e15d188220a5f3aa1b55955d74e1b78
6
+ metadata.gz: 03ddf93849556da343a19662e80dd124dc0919fae3b6ff6a3bd9b0c248e1c45a8bec754cb698924e5561ea22c592e7598ebb9c082d77aec6172ac188f2ed9693
7
+ data.tar.gz: 17a6c92634f5e3a40a87f1d3c435692de0837ba0dcfa8423994732af3716028a4885220f38e9e746eee12c13c96366002828372b507b656d196bea548542b444
@@ -0,0 +1,73 @@
1
+ require "rails/generators"
2
+ require "active_record"
3
+ require "fileutils"
4
+
5
+ module MultiTenantSubdomain
6
+ class MultiTenantSubdomainGenerator < Rails::Generators::Base
7
+ source_root File.expand_path("templates", __dir__)
8
+ argument :model_name, type: :string
9
+
10
+ def create_model
11
+ return if behavior != :invoke
12
+
13
+ if model_exists?
14
+ add_subdomain_field if migration_needed?
15
+ else
16
+ generate "model", "#{model_name} subdomain:string:index"
17
+ end
18
+
19
+ create_initializer
20
+ end
21
+
22
+ def destroy
23
+ return if behavior != :revoke
24
+
25
+ remove_initializer
26
+ remove_model if model_exists?
27
+ remove_migrations
28
+ end
29
+
30
+ private
31
+
32
+ def model_exists?
33
+ File.exist?(Rails.root.join("app", "models", "#{model_name.underscore}.rb"))
34
+ end
35
+
36
+ def table_exists?
37
+ ActiveRecord::Base.connection.data_source_exists?(model_name.tableize)
38
+ end
39
+
40
+ def column_exists?(column)
41
+ return false unless table_exists?
42
+
43
+ ActiveRecord::Base.connection.column_exists?(model_name.tableize, column)
44
+ end
45
+
46
+ def migration_needed?
47
+ table_exists? && !column_exists?(:subdomain)
48
+ end
49
+
50
+ def add_subdomain_field
51
+ timestamp = Time.now.strftime("%Y%m%d%H%M%S")
52
+ migration_file = "db/migrate/#{timestamp}_add_subdomain_to_#{model_name.tableize}.rb"
53
+
54
+ create_file migration_file, <<~RUBY
55
+ class AddSubdomainTo#{model_name.camelize} < ActiveRecord::Migration[#{Rails.version.to_f}]
56
+ def change
57
+ add_column :#{model_name.tableize}, :subdomain, :string, index: true
58
+ end
59
+ end
60
+ RUBY
61
+ end
62
+
63
+ def create_initializer
64
+ initializer_file = "config/initializers/multi_tenant_subdomain.rb"
65
+
66
+ create_file initializer_file, <<~RUBY
67
+ MultiTenantSubdomain.configure do |config|
68
+ config.tenant_model = "#{model_name}"
69
+ end
70
+ RUBY
71
+ end
72
+ end
73
+ end
@@ -10,10 +10,10 @@ module MultiTenantSubdomain
10
10
  def create_model
11
11
  return if behavior != :invoke
12
12
 
13
- unless model_exists?
14
- generate "model", "#{model_name} subdomain:string:index"
15
- else
13
+ if model_exists?
16
14
  add_subdomain_field if migration_needed?
15
+ else
16
+ generate "model", "#{model_name} subdomain:string:index"
17
17
  end
18
18
 
19
19
  create_initializer
@@ -21,6 +21,7 @@ module MultiTenantSubdomain
21
21
 
22
22
  def destroy
23
23
  return if behavior != :revoke
24
+
24
25
  remove_initializer
25
26
  remove_model if model_exists?
26
27
  remove_migrations
@@ -62,23 +63,19 @@ module MultiTenantSubdomain
62
63
  def create_initializer
63
64
  initializer_path = Rails.root.join("config", "initializers", "multi_tenant_subdomain.rb")
64
65
 
65
- unless File.exist?(initializer_path)
66
- create_file initializer_path, <<~RUBY
67
- MultiTenantSubdomain.configure do |config|
68
- config.tenant_model = #{model_name.to_s.camelize}
69
- end
70
- RUBY
71
- else
66
+ if File.exist?(initializer_path)
72
67
  puts "X Initializer already exists. Skipping..."
68
+ else
69
+ template "multi_tenant_subdomain.rb", initializer_path
73
70
  end
74
71
  end
75
72
 
76
73
  def remove_initializer
77
74
  initializer_path = Rails.root.join("config", "initializers", "multi_tenant_subdomain.rb")
78
- if File.exist?(initializer_path)
79
- FileUtils.rm_f(initializer_path)
80
- puts "X Removed initializer: config/initializers/multi_tenant_subdomain.rb"
81
- end
75
+ return unless File.exist?(initializer_path)
76
+
77
+ FileUtils.rm_f(initializer_path)
78
+ puts "X Removed initializer: config/initializers/multi_tenant_subdomain.rb"
82
79
  end
83
80
 
84
81
  def remove_model
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ MultiTenantSubdomain.configure do |config|
4
+ config.tenant_model_class = "<%= model_name.to_s.camelize %>"
5
+ config.tenant_model_table = "<%= model_name.tableize %>"
6
+ config.tenant_model_pk = "id"
7
+ config.tenant_model_fk = "<%= model_name.underscore %>_id"
8
+ end
@@ -1,18 +1,79 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module MultiTenantSubdomain
4
+ # This module is used to extend ActiveRecord models to support multi-tenant
5
+ # subdomains.
6
+ #
7
+ # It adds a default scope to the model to ensure that all records are
8
+ # scoped to the current tenant.
9
+ #
10
+ # It also adds a before_validation callback to set the tenant_id on the
11
+ # model.
2
12
  module ActiveRecordExtension
3
13
  extend ActiveSupport::Concern
4
14
 
5
15
  included do
6
- default_scope { where(tenant_id: MultiTenantSubdomain::TenantManager.current_tenant&.id) }
7
- before_validation :set_tenant_id
16
+ # Don't try to access the database during class loading
17
+ # Instead, set up a hook that will be called when the model is used
18
+ after_initialize :ensure_tenant_configured, if: :tenant_configured?
19
+
20
+ # Class method to temporarily disable tenant scoping
21
+ define_singleton_method(
22
+ "without_#{MultiTenantSubdomain.config.tenant_model_class.to_s.underscore}_scope".to_sym
23
+ ) do |&block|
24
+ unscoped(&block)
25
+ end
26
+ end
27
+
28
+ class_methods do
29
+ def tenant_configured?
30
+ # Safely check if we can access table information without raising errors
31
+ begin
32
+ connected? && table_exists? &&
33
+ column_names.include?(MultiTenantSubdomain.config.tenant_model_fk)
34
+ rescue => e
35
+ false
36
+ end
37
+ end
38
+
39
+ # Apply tenant scoping when the model is used
40
+ def apply_tenant_scoping
41
+ # Only apply if not already applied
42
+ return if instance_variable_defined?(:@tenant_scoping_applied)
43
+
44
+ before_validation :set_tenant_id
45
+ default_scope do
46
+ where(
47
+ MultiTenantSubdomain.config.tenant_model_fk =>
48
+ MultiTenantSubdomain::TenantManager.current_tenant.send(
49
+ MultiTenantSubdomain.config.tenant_model_pk.to_sym
50
+ )
51
+ )
52
+ end
53
+ @tenant_scoping_applied = true
54
+ end
8
55
  end
9
56
 
10
57
  private
11
58
 
59
+ def tenant_configured?
60
+ self.class.tenant_configured?
61
+ end
62
+
63
+ def ensure_tenant_configured
64
+ self.class.apply_tenant_scoping
65
+ end
66
+
12
67
  def set_tenant_id
13
- self.tenant_id = MultiTenantSubdomain::TenantManager.current_tenant&.id
68
+ self[MultiTenantSubdomain.config.tenant_model_fk.to_sym] = MultiTenantSubdomain::TenantManager.current_tenant.send(
69
+ MultiTenantSubdomain.config.tenant_model_pk.to_sym
70
+ )
14
71
  end
15
72
  end
16
73
  end
17
74
 
18
- ActiveRecord::Base.include MultiTenantSubdomain::ActiveRecordExtension
75
+ # Use ActiveSupport.on_load to include the module in ActiveRecord::Base
76
+ # This ensures that the module is loaded at the right time in the Rails initialization process
77
+ ActiveSupport.on_load(:active_record) do
78
+ include MultiTenantSubdomain::ActiveRecordExtension
79
+ end
@@ -1,11 +1,28 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module MultiTenantSubdomain
4
+ # This middleware is used to set the current tenant based on the subdomain
5
+ # of the request.
6
+ #
7
+ # It is used to ensure that all requests are scoped to a single tenant.
2
8
  class Middleware
3
9
  def initialize(app)
4
10
  @app = app
5
11
  end
6
12
 
7
13
  def call(env)
8
- tenant = extract_subdomain(env)
14
+ subdomain = extract_subdomain(env)
15
+ tenant = MultiTenantSubdomain.config.tenant_model_table.classify.constantize.find_by(subdomain: subdomain)
16
+
17
+ if tenant.nil? && subdomain.present?
18
+ # Option 1: Return a 404 Not Found response
19
+ return [404, {'Content-Type' => 'text/html'}, ['Tenant not found']]
20
+
21
+ # Option 2: Redirect to main domain (uncomment to use)
22
+ # main_domain = env['HTTP_HOST'].split('.').drop(1).join('.')
23
+ # return [302, {'Location' => "https://#{main_domain}"}, []]
24
+ end
25
+
9
26
  MultiTenantSubdomain::TenantManager.current_tenant = tenant
10
27
  @app.call(env)
11
28
  ensure
@@ -18,13 +35,13 @@ module MultiTenantSubdomain
18
35
 
19
36
  def extract_subdomain(env)
20
37
  request = Rack::Request.new(env)
21
- host_parts = request.host.split('.')
22
-
38
+ host_parts = request.host.split(".")
39
+
23
40
  # Avoid affecting primary domain
24
41
  return nil if host_parts.length < 3
25
42
 
26
43
  # Avoid affecting www subdomain
27
- return nil if host_parts.first == 'www'
44
+ return nil if host_parts.first == "www"
28
45
 
29
46
  host_parts.first
30
47
  end
@@ -1,4 +1,9 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module MultiTenantSubdomain
4
+ # This class is used to manage the current tenant.
5
+ #
6
+ # It is used to ensure that all requests are scoped to a single tenant.
2
7
  class TenantManager
3
8
  MTS_KEY = :current_tenant
4
9
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module MultiTenantSubdomain
4
- VERSION = "0.1.0"
4
+ VERSION = "0.1.1"
5
5
  end
@@ -7,21 +7,89 @@ require_relative "multi_tenant_subdomain/middleware"
7
7
  require_relative "multi_tenant_subdomain/active_record_extension"
8
8
  require_relative "generators/multi_tenant_subdomain/multi_tenant_subdomain_generator"
9
9
 
10
+ ##
11
+ # This module provides multi-tenant subdomain functionality for Rails applications.
12
+ #
13
+ # It allows you to easily scope your application's data and functionality
14
+ # to different tenants based on the subdomain in the request URL.
15
+ #
16
+ # To use this module, you need to:
17
+ #
18
+ # 1. Generate the necessary migrations and configurations using the provided generator:
19
+ #
20
+ # `rails generate multi_tenant_subdomain YourTenantModelName`
21
+ #
22
+ # 2. Configure the module with your tenant model class and primary/foreign keys:
23
+ #
24
+ # ```ruby
25
+ # MultiTenantSubdomain.configure do |config|
26
+ # config.tenant_model_class = "Account" # Default: "Tenant"
27
+ # config.tenant_model_table = "accounts" # Default: "tenants"
28
+ # config.tenant_model_pk = "id" # Default: "id"
29
+ # config.tenant_model_fk = "account_id" # Default: "tenant_id"
30
+ # end
31
+ # ```
32
+ #
33
+ # 3. Use the `MultiTenantSubdomain::TenantManager.current_tenant` method to access the
34
+ # current tenant in your controllers and views.
10
35
  module MultiTenantSubdomain
36
+ ##
37
+ # Error class for any errors raised by the module.
11
38
  class Error < StandardError; end
39
+
40
+ ##
41
+ # Configuration class for the module.
42
+ class Configuration
43
+ ##
44
+ # @!attribute tenant_model_class
45
+ # @return [String] The name of the tenant model class. Default: "Tenant".
46
+ attr_accessor :tenant_model_class
47
+
48
+ ##
49
+ # @!attribute tenant_model_table
50
+ # @return [String] The name of the tenant model table. Default: "tenants".
51
+ attr_accessor :tenant_model_table
52
+
53
+ ##
54
+ # @!attribute tenant_model_pk
55
+ # @return [String] The name of the primary key column on the tenant model. Default: "id".
56
+ attr_accessor :tenant_model_pk
57
+
58
+ ##
59
+ # @!attribute tenant_model_fk
60
+ # @return [String] The name of the foreign key column on the models that should be scoped to a tenant. Default: "tenant_id".
61
+ attr_accessor :tenant_model_fk
62
+
63
+ # Initializes a new Configuration object with default values.
64
+ def initialize
65
+ @tenant_model_class = "Tenant"
66
+ @tenant_model_table = "tenants"
67
+ @tenant_model_pk = "id"
68
+ @tenant_model_fk = "tenant_id"
69
+ end
70
+ end
71
+
12
72
  class << self
73
+ ##
74
+ # @!attribute config
75
+ # @return [Configuration] The configuration object for the module.
13
76
  attr_accessor :config
14
77
 
78
+ ##
79
+ # Configures the module.
80
+ #
81
+ # @yield [config] The configuration object.
82
+ # @yieldparam config [Configuration] The configuration object.
83
+ #
84
+ # @example
85
+ # MultiTenantSubdomain.configure do |config|
86
+ # config.tenant_model_class = "Account"
87
+ # config.tenant_model_pk = "account_id"
88
+ # config.tenant_model_fk = "account_id"
89
+ # end
15
90
  def configure
16
91
  self.config ||= Configuration.new
17
92
  yield(config) if block_given?
18
93
  end
19
94
  end
20
- class Configuration
21
- attr_accessor :tenant_model
22
-
23
- def initialize
24
- @tenant_model = "Tenant"
25
- end
26
- end
27
95
  end
Binary file
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: multi_tenant_subdomain
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sharvy Ahmed
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2025-02-24 00:00:00.000000000 Z
10
+ date: 2025-02-28 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: rspec
@@ -80,13 +80,16 @@ files:
80
80
  - LICENSE.txt
81
81
  - README.md
82
82
  - Rakefile
83
+ - generators/multi_tenant_subdomain/multi_tenant_subdomain_generator.rb
83
84
  - lib/generators/multi_tenant_subdomain/multi_tenant_subdomain_generator.rb
85
+ - lib/generators/multi_tenant_subdomain/templates/multi_tenant_subdomain.rb
84
86
  - lib/multi_tenant_subdomain.rb
85
87
  - lib/multi_tenant_subdomain/active_record_extension.rb
86
88
  - lib/multi_tenant_subdomain/middleware.rb
87
89
  - lib/multi_tenant_subdomain/railtie.rb
88
90
  - lib/multi_tenant_subdomain/tenant_manager.rb
89
91
  - lib/multi_tenant_subdomain/version.rb
92
+ - multi_tenant_subdomain-0.1.0.gem
90
93
  - sig/multi_tenant_subdomain.rbs
91
94
  homepage: https://github.com/sharvy/multi_tenant_subdomain
92
95
  licenses: