pg_multitenant_schemas 0.2.3 → 0.3.6

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.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/.agent.md +54 -0
  3. data/.rubocop.yml +7 -1
  4. data/.ruby-version +1 -1
  5. data/CHANGELOG.md +58 -0
  6. data/DELIVERY_SUMMARY.md +318 -0
  7. data/GITHUB_ACTIONS_QUICK_START.md +110 -0
  8. data/GITHUB_ACTIONS_SETUP.md +313 -0
  9. data/GITHUB_ACTIONS_SUMMARY.md +231 -0
  10. data/LICENSE.txt +1 -1
  11. data/LOCAL_TESTING_SUMMARY.md +30 -13
  12. data/READY_TO_RELEASE.md +308 -0
  13. data/RELEASE_NOTES.md +194 -0
  14. data/RELEASE_STEPS.md +122 -0
  15. data/WORKFLOW_FIXES.md +188 -0
  16. data/docs/index.html +929 -0
  17. data/lib/pg_multitenant_schemas/configuration.rb +7 -1
  18. data/lib/pg_multitenant_schemas/context.rb +92 -3
  19. data/lib/pg_multitenant_schemas/rails/controller_concern.rb +81 -6
  20. data/lib/pg_multitenant_schemas/rails/generators/install_generator.rb +27 -0
  21. data/lib/pg_multitenant_schemas/rails/generators/tenant_migration_generator.rb +30 -0
  22. data/lib/pg_multitenant_schemas/rails/model_concern.rb +3 -3
  23. data/lib/pg_multitenant_schemas/rails/railtie.rb +27 -0
  24. data/lib/pg_multitenant_schemas/schema_switcher.rb +73 -7
  25. data/lib/pg_multitenant_schemas/tenant_resolver.rb +3 -3
  26. data/lib/pg_multitenant_schemas/ui/app/assets/stylesheets/pg_multitenant_schemas/ui/application.css +94 -0
  27. data/lib/pg_multitenant_schemas/ui/app/controllers/pg_multitenant_schemas/ui/tenants_controller.rb +130 -0
  28. data/lib/pg_multitenant_schemas/ui/app/views/layouts/pg_multitenant_schemas/ui/application.html.erb +29 -0
  29. data/lib/pg_multitenant_schemas/ui/app/views/pg_multitenant_schemas/ui/tenants/index.html.erb +85 -0
  30. data/lib/pg_multitenant_schemas/ui/app/views/pg_multitenant_schemas/ui/tenants/migration_status.html.erb +45 -0
  31. data/lib/pg_multitenant_schemas/ui/app/views/pg_multitenant_schemas/ui/tenants/new.html.erb +50 -0
  32. data/lib/pg_multitenant_schemas/ui/config/routes.rb +20 -0
  33. data/lib/pg_multitenant_schemas/ui/engine.rb +49 -0
  34. data/lib/pg_multitenant_schemas/ui.rb +9 -0
  35. data/lib/pg_multitenant_schemas/version.rb +1 -1
  36. data/lib/pg_multitenant_schemas.rb +155 -29
  37. data/pg_multitenant_schemas.gemspec +15 -9
  38. data/pre-release-check.sh +46 -0
  39. metadata +39 -12
@@ -0,0 +1,130 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgMultitenantSchemas
4
+ module UI
5
+ # Controller backing the UI engine for listing and managing tenants and schemas.
6
+ class TenantsController < ActionController::Base
7
+ layout "pg_multitenant_schemas/ui/application"
8
+ skip_forgery_protection only: :destroy
9
+
10
+ before_action :load_tenant_model
11
+
12
+ def index
13
+ @tenants = tenant_model.all.order(:subdomain)
14
+ @schemas = load_available_schemas
15
+ @current_schema = resolve_selected_schema
16
+ end
17
+
18
+ def new
19
+ @tenant = tenant_model.new
20
+ end
21
+
22
+ def create
23
+ @tenant = tenant_model.new(tenant_params)
24
+
25
+ if @tenant.save
26
+ PgMultitenantSchemas::Migrator.setup_tenant(@tenant.subdomain, verbose: false)
27
+ redirect_to "/pg_multitenant_schemas/tenants",
28
+ notice: "Tenant '#{@tenant.subdomain}' created and schema provisioned."
29
+ else
30
+ render :new, status: :unprocessable_entity
31
+ end
32
+ rescue StandardError => e
33
+ @tenant&.destroy if @tenant&.persisted?
34
+ redirect_to "/pg_multitenant_schemas/tenants",
35
+ alert: "Failed to provision schema: #{e.message}"
36
+ end
37
+
38
+ def destroy
39
+ @tenant = tenant_model.find(params[:id])
40
+ schema = @tenant.subdomain
41
+
42
+ @tenant.destroy
43
+ PgMultitenantSchemas::SchemaSwitcher.drop_schema(schema)
44
+ redirect_to "/pg_multitenant_schemas/tenants",
45
+ notice: "Tenant '#{schema}' and its schema deleted."
46
+ rescue StandardError => e
47
+ redirect_to "/pg_multitenant_schemas/tenants",
48
+ alert: "Error deleting tenant: #{e.message}"
49
+ end
50
+
51
+ def migrate
52
+ @tenant = tenant_model.find(params[:id])
53
+ result = PgMultitenantSchemas::Migrator.migrate_tenant(@tenant.subdomain, verbose: false)
54
+ status = result[:status] == :success ? "notice" : "alert"
55
+ msg = result[:message] || "Migration completed."
56
+ redirect_to "/pg_multitenant_schemas/tenants", status => msg
57
+ rescue StandardError => e
58
+ redirect_to "/pg_multitenant_schemas/tenants", alert: "Migration failed: #{e.message}"
59
+ end
60
+
61
+ def migrate_all
62
+ results = PgMultitenantSchemas::Migrator.migrate_all(verbose: false)
63
+ failed = results.count { |r| r[:status] == :error }
64
+ if failed.zero?
65
+ redirect_to "/pg_multitenant_schemas/tenants",
66
+ notice: "All #{results.size} tenant schemas migrated successfully."
67
+ else
68
+ redirect_to "/pg_multitenant_schemas/tenants",
69
+ alert: "#{failed} of #{results.size} migrations failed."
70
+ end
71
+ end
72
+
73
+ def migration_status
74
+ @tenant = tenant_model.find(params[:id])
75
+ statuses = PgMultitenantSchemas::Migrator.migration_status(verbose: false)
76
+ @status = statuses.find { |s| s[:schema] == @tenant.subdomain } || {}
77
+ render :migration_status
78
+ end
79
+
80
+ def switch
81
+ schema = params[:schema].to_s.strip
82
+ return redirect_invalid_schema(schema) unless PgMultitenantSchemas::SchemaSwitcher.schema_exists?(schema)
83
+
84
+ PgMultitenantSchemas::Context.current_schema = schema
85
+ store_schema_in_session_and_cookies(schema)
86
+ redirect_to "/pg_multitenant_schemas/tenants",
87
+ notice: "Switched active context to schema '#{schema}'."
88
+ end
89
+
90
+ private
91
+
92
+ def load_available_schemas
93
+ PgMultitenantSchemas::SchemaSwitcher.list_schemas
94
+ rescue StandardError
95
+ []
96
+ end
97
+
98
+ def resolve_selected_schema
99
+ selected = (session[:pg_multitenant_selected_schema] || cookies[:pg_multitenant_selected_schema]).to_s.strip
100
+ return selected if selected.present? && load_available_schemas.include?(selected)
101
+
102
+ PgMultitenantSchemas::Context.current_schema
103
+ end
104
+
105
+ def store_schema_in_session_and_cookies(schema)
106
+ session[:pg_multitenant_selected_schema] = schema
107
+ cookies[:pg_multitenant_selected_schema] = {
108
+ value: schema,
109
+ expires: 24.hours.from_now,
110
+ path: "/",
111
+ same_site: :lax,
112
+ http_only: false
113
+ }
114
+ end
115
+
116
+ def redirect_invalid_schema(schema)
117
+ redirect_to "/pg_multitenant_schemas/tenants",
118
+ alert: "Schema '#{schema}' does not exist."
119
+ end
120
+
121
+ def load_tenant_model
122
+ @tenant_model_name = PgMultitenantSchemas.configuration.tenant_model_class
123
+ end
124
+
125
+ def tenant_model = @tenant_model_name.constantize
126
+
127
+ def tenant_params = params.require(:tenant).permit(:subdomain, :name, :status)
128
+ end
129
+ end
130
+ end
@@ -0,0 +1,29 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>Tenant Manager · PgMultitenantSchemas</title>
7
+ <%= csrf_meta_tags %>
8
+ <style>
9
+ <%= File.read(File.expand_path("../../../../assets/stylesheets/pg_multitenant_schemas/ui/application.css", __dir__)).html_safe %>
10
+ </style>
11
+ </head>
12
+ <body>
13
+ <header>
14
+ <h1>🏢 Tenant Manager</h1>
15
+ <span>pg_multitenant_schemas</span>
16
+ </header>
17
+
18
+ <main>
19
+ <% if flash[:notice] %>
20
+ <div class="flash notice"><%= flash[:notice] %></div>
21
+ <% end %>
22
+ <% if flash[:alert] %>
23
+ <div class="flash alert"><%= flash[:alert] %></div>
24
+ <% end %>
25
+
26
+ <%= yield %>
27
+ </main>
28
+ </body>
29
+ </html>
@@ -0,0 +1,85 @@
1
+ <%# Switch active schema context %>
2
+ <div class="switch-bar">
3
+ <label>Active schema:</label>
4
+ <span class="schema-badge" id="current-schema-badge"><%= @current_schema %></span>
5
+
6
+ <%= form_with url: "/pg_multitenant_schemas/tenants/switch", method: :post, local: true do |f| %>
7
+ <%= f.select :schema,
8
+ options_for_select(@schemas, @current_schema),
9
+ { include_blank: false },
10
+ class: "",
11
+ id: "schema-select" %>
12
+ <%= f.submit "Switch →", class: "btn btn-ghost btn-sm" %>
13
+ <% end %>
14
+ </div>
15
+
16
+ <script>
17
+ // Update badge when selection changes
18
+ document.getElementById('schema-select')?.addEventListener('change', function(e) {
19
+ document.getElementById('current-schema-badge').textContent = e.target.value;
20
+ });
21
+ </script>
22
+
23
+ <%# Toolbar %>
24
+ <div class="toolbar" style="margin-bottom:1rem;">
25
+ <%= link_to "+ New Tenant", "/pg_multitenant_schemas/tenants/new", class: "btn btn-primary" %>
26
+
27
+ <%= form_with url: "/pg_multitenant_schemas/tenants/migrate_all", method: :post, local: true do |f| %>
28
+ <%= f.submit "⚡ Migrate All", class: "btn btn-yellow",
29
+ data: { confirm: "Run pending migrations on all tenant schemas?" } %>
30
+ <% end %>
31
+ </div>
32
+
33
+ <%# Tenant table %>
34
+ <div class="card">
35
+ <div class="card-header">
36
+ <h2>Tenants <span style="font-weight:400;color:#9ca3af;">(<%= @tenants.count %>)</span></h2>
37
+ </div>
38
+
39
+ <% if @tenants.empty? %>
40
+ <div class="empty">No tenants yet. Create your first one above.</div>
41
+ <% else %>
42
+ <table>
43
+ <thead>
44
+ <tr>
45
+ <th>Subdomain</th>
46
+ <th>Name</th>
47
+ <th>Status</th>
48
+ <th>Actions</th>
49
+ </tr>
50
+ </thead>
51
+ <tbody>
52
+ <% @tenants.each do |tenant| %>
53
+ <tr>
54
+ <td><span class="schema-badge"><%= tenant.subdomain %></span></td>
55
+ <td><%= tenant.try(:name) || "—" %></td>
56
+ <td>
57
+ <% status = tenant.try(:status).to_s %>
58
+ <% badge_class = { "active" => "badge-green", "trial" => "badge-yellow",
59
+ "inactive" => "badge-gray", "suspended" => "badge-red" }[status] || "badge-gray" %>
60
+ <span class="badge <%= badge_class %>"><%= status.presence || "—" %></span>
61
+ </td>
62
+ <td>
63
+ <div class="actions">
64
+ <%= link_to "📊 Status",
65
+ "/pg_multitenant_schemas/tenants/#{tenant.id}/migration_status",
66
+ class: "btn btn-ghost btn-sm" %>
67
+
68
+ <%= form_with url: "/pg_multitenant_schemas/tenants/#{tenant.id}/migrate",
69
+ method: :post, local: true, style: "display:inline" do |f| %>
70
+ <%= f.submit "▶ Migrate", class: "btn btn-green btn-sm" %>
71
+ <% end %>
72
+
73
+ <%= form_with url: "/pg_multitenant_schemas/tenants/#{tenant.id}",
74
+ method: :delete, local: true, style: "display:inline" do |f| %>
75
+ <%= f.submit "✕ Delete", class: "btn btn-red btn-sm",
76
+ data: { confirm: "Delete tenant '#{tenant.subdomain}' and drop its schema? This is irreversible." } %>
77
+ <% end %>
78
+ </div>
79
+ </td>
80
+ </tr>
81
+ <% end %>
82
+ </tbody>
83
+ </table>
84
+ <% end %>
85
+ </div>
@@ -0,0 +1,45 @@
1
+ <%= link_to "← Back to Tenants", "/pg_multitenant_schemas/tenants", class: "btn btn-ghost btn-sm", style: "margin-bottom:1rem;display:inline-flex;" %>
2
+
3
+ <div class="card" style="max-width:560px;">
4
+ <div class="card-header">
5
+ <h2>Migration Status — <span class="schema-badge"><%= @tenant.subdomain %></span></h2>
6
+ </div>
7
+
8
+ <div style="padding:1.25rem;display:flex;flex-direction:column;gap:1rem;">
9
+ <% if @status[:error] %>
10
+ <div class="flash alert">Error: <%= @status[:error] %></div>
11
+ <% else %>
12
+ <table>
13
+ <tbody>
14
+ <tr>
15
+ <td style="color:#6b7280;font-size:.8rem;font-weight:600;text-transform:uppercase;">Status</td>
16
+ <td>
17
+ <% if @status[:status] == :up_to_date %>
18
+ <span class="badge badge-green">✓ Up to date</span>
19
+ <% elsif @status[:status] == :pending %>
20
+ <span class="badge badge-yellow">⚠ Pending</span>
21
+ <% else %>
22
+ <span class="badge badge-gray"><%= @status[:status] %></span>
23
+ <% end %>
24
+ </td>
25
+ </tr>
26
+ <tr>
27
+ <td style="color:#6b7280;font-size:.8rem;font-weight:600;text-transform:uppercase;">Applied</td>
28
+ <td><strong><%= @status[:applied_count] || 0 %></strong> migrations</td>
29
+ </tr>
30
+ <tr>
31
+ <td style="color:#6b7280;font-size:.8rem;font-weight:600;text-transform:uppercase;">Pending</td>
32
+ <td><strong><%= @status[:pending_count] || 0 %></strong> migrations</td>
33
+ </tr>
34
+ </tbody>
35
+ </table>
36
+
37
+ <% if @status[:status] == :pending %>
38
+ <%= form_with url: "/pg_multitenant_schemas/tenants/#{@tenant.id}/migrate",
39
+ method: :post, local: true do |f| %>
40
+ <%= f.submit "▶ Run Migrations Now", class: "btn btn-green" %>
41
+ <% end %>
42
+ <% end %>
43
+ <% end %>
44
+ </div>
45
+ </div>
@@ -0,0 +1,50 @@
1
+ <%= link_to "← Back to Tenants", "/pg_multitenant_schemas/tenants", class: "btn btn-ghost btn-sm", style: "margin-bottom:1rem;display:inline-flex;" %>
2
+
3
+ <div class="card" style="max-width:480px;">
4
+ <div class="card-header"><h2>New Tenant</h2></div>
5
+
6
+ <%= form_with model: @tenant,
7
+ url: "/pg_multitenant_schemas/tenants",
8
+ local: true,
9
+ style: "padding:1.25rem;display:flex;flex-direction:column;gap:1rem;" do |f| %>
10
+
11
+ <% if @tenant.errors.any? %>
12
+ <div class="flash alert">
13
+ <ul style="list-style:none;">
14
+ <% @tenant.errors.full_messages.each do |msg| %>
15
+ <li>⚠ <%= msg %></li>
16
+ <% end %>
17
+ </ul>
18
+ </div>
19
+ <% end %>
20
+
21
+ <div style="display:flex;flex-direction:column;gap:.35rem;">
22
+ <%= f.label :subdomain, "Subdomain / Schema name", style: "font-size:.85rem;font-weight:500;" %>
23
+ <%= f.text_field :subdomain,
24
+ placeholder: "e.g. acme_corp",
25
+ style: "border:1px solid #d1d5db;border-radius:6px;padding:.5rem .75rem;font-size:.9rem;",
26
+ required: true %>
27
+ <small style="color:#6b7280;font-size:.78rem;">Used as the PostgreSQL schema name. Lowercase letters, numbers, underscores only.</small>
28
+ </div>
29
+
30
+ <div style="display:flex;flex-direction:column;gap:.35rem;">
31
+ <%= f.label :name, "Display name", style: "font-size:.85rem;font-weight:500;" %>
32
+ <%= f.text_field :name,
33
+ placeholder: "e.g. ACME Corporation",
34
+ style: "border:1px solid #d1d5db;border-radius:6px;padding:.5rem .75rem;font-size:.9rem;" %>
35
+ </div>
36
+
37
+ <div style="display:flex;flex-direction:column;gap:.35rem;">
38
+ <%= f.label :status, "Status", style: "font-size:.85rem;font-weight:500;" %>
39
+ <%= f.select :status,
40
+ [["Active", "active"], ["Trial", "trial"], ["Inactive", "inactive"]],
41
+ { selected: "active" },
42
+ style: "border:1px solid #d1d5db;border-radius:6px;padding:.5rem .75rem;font-size:.9rem;" %>
43
+ </div>
44
+
45
+ <div style="display:flex;gap:.6rem;padding-top:.25rem;">
46
+ <%= f.submit "Create Tenant & Provision Schema", class: "btn btn-primary" %>
47
+ <%= link_to "Cancel", "/pg_multitenant_schemas/tenants", class: "btn btn-ghost" %>
48
+ </div>
49
+ <% end %>
50
+ </div>
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ PgMultitenantSchemas::UI::Engine.routes.draw do
4
+ root to: "tenants#index"
5
+
6
+ resources :tenants, only: %i[index new create destroy] do
7
+ member do
8
+ post :migrate
9
+ get :migration_status
10
+ end
11
+ collection do
12
+ post :migrate_all
13
+ post :switch
14
+ end
15
+ end
16
+
17
+ # Fallback for API-only apps where Rack::MethodOverride may be absent.
18
+ # Keeps the same path used by RESTful DELETE forms, but accepts POST.
19
+ post "/tenants/:id", to: "tenants#destroy"
20
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgMultitenantSchemas
4
+ module UI
5
+ # Mountable Rails engine that exposes the tenant management UI.
6
+ class Engine < ::Rails::Engine
7
+ isolate_namespace PgMultitenantSchemas::UI
8
+
9
+ # Tell Rails where this engine's app/ directory lives
10
+ config.root = "#{File.expand_path("../../..", __dir__)}/lib/pg_multitenant_schemas/ui"
11
+
12
+ # Fix Zeitwerk inflection: 'ui' directory should map to 'UI' constant
13
+ initializer "pg_multitenant_schemas.ui.inflections", before: :load_config_initializers do
14
+ ActiveSupport::Inflector.inflections(:en) do |inflect|
15
+ inflect.acronym "UI"
16
+ end
17
+ end
18
+
19
+ initializer "pg_multitenant_schemas.ui.middleware" do |app|
20
+ # Ensure cookies and session middleware are available for cookie-based schema switching
21
+ # Only in development mode to support schema switching between dashboard and main app
22
+ if ::Rails.env.development?
23
+ # Try to add middleware, but don't fail if it's already there
24
+ begin
25
+ app.config.middleware.insert_before 0, ActionDispatch::Cookies
26
+ rescue StandardError
27
+ # Middleware may already exist, that's fine
28
+ end
29
+
30
+ begin
31
+ app.config.middleware.insert_before 1, ActionDispatch::Session::CookieStore, key: "_pg_multitenant_session"
32
+ rescue StandardError
33
+ # Middleware may already exist, that's fine
34
+ end
35
+
36
+ begin
37
+ app.config.middleware.insert_before 2, ActionDispatch::Flash
38
+ rescue StandardError
39
+ # Middleware may already exist, that's fine
40
+ end
41
+ end
42
+ end
43
+
44
+ initializer "pg_multitenant_schemas.ui.assets" do |app|
45
+ app.config.assets.precompile += %w[pg_multitenant_schemas/ui/application.css] if app.config.respond_to?(:assets)
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgMultitenantSchemas
4
+ # Namespace for the mountable UI engine that manages tenants and schemas.
5
+ module UI
6
+ end
7
+ end
8
+
9
+ require_relative "ui/engine"
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PgMultitenantSchemas
4
- VERSION = "0.2.3"
4
+ VERSION = "0.3.6"
5
5
  end
@@ -1,38 +1,164 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # frozen_string_li# PgMultitenantSchemas provides modern PostgreSQL schema-based multitenancy functionality.
4
- # Built for Rails 8+ and Ruby 3.4+, it allows switching between different PostgreSQL schemas
5
- # to achieve secure tenant isolation while maintaining high performance and developer experience.al: true
6
-
7
- require "active_support/core_ext/module/delegation"
8
- require "active_support/core_ext/object/blank"
9
- require_relative "pg_multitenant_schemas/version"
10
- require_relative "pg_multitenant_schemas/errors"
11
- require_relative "pg_multitenant_schemas/configuration"
12
- require_relative "pg_multitenant_schemas/context"
13
- require_relative "pg_multitenant_schemas/schema_switcher"
14
- require_relative "pg_multitenant_schemas/tenant_resolver"
15
- require_relative "pg_multitenant_schemas/migrator"
16
-
17
- # Rails integration (Rails 8+ required)
18
- if defined?(Rails)
19
- require_relative "pg_multitenant_schemas/rails/controller_concern"
20
- require_relative "pg_multitenant_schemas/rails/model_concern"
21
- require_relative "pg_multitenant_schemas/rails/railtie"
22
- end
23
-
24
3
  # PgMultitenantSchemas provides modern PostgreSQL schema-based multitenancy functionality.
4
+ #
25
5
  # Built for Rails 8+ and Ruby 3.3+, it allows switching between different PostgreSQL schemas
26
6
  # to achieve secure tenant isolation while maintaining high performance and developer experience.
7
+ #
8
+ # @example Basic usage
9
+ # tenant = Tenant.find(1)
10
+ # PgMultitenantSchemas.switch_to_tenant(tenant)
11
+ # # All queries now use tenant's schema
12
+ #
13
+ # @example Block-based context
14
+ # PgMultitenantSchemas.with_tenant(tenant) do
15
+ # User.all # Queries tenant's schema
16
+ # end
17
+ #
18
+ # @see PgMultitenantSchemas::Context
19
+ # @see PgMultitenantSchemas::SchemaSwitcher
20
+ # @see PgMultitenantSchemas::Migrator
27
21
  module PgMultitenantSchemas
22
+ require "active_support/core_ext/module/delegation"
23
+ require "active_support/core_ext/object/blank"
24
+ require_relative "pg_multitenant_schemas/version"
25
+ require_relative "pg_multitenant_schemas/errors"
26
+ require_relative "pg_multitenant_schemas/configuration"
27
+ require_relative "pg_multitenant_schemas/context"
28
+ require_relative "pg_multitenant_schemas/schema_switcher"
29
+ require_relative "pg_multitenant_schemas/tenant_resolver"
30
+ require_relative "pg_multitenant_schemas/migrator"
31
+
32
+ # Rails integration (Rails 8+ required)
33
+ if defined?(Rails)
34
+ require_relative "pg_multitenant_schemas/rails/controller_concern"
35
+ require_relative "pg_multitenant_schemas/rails/model_concern"
36
+ require_relative "pg_multitenant_schemas/rails/railtie"
37
+ require_relative "pg_multitenant_schemas/ui/engine"
38
+ end
39
+
28
40
  class << self
29
- # Delegate common methods to Context for convenience
30
- delegate :current_tenant, :current_tenant=, :current_schema, :current_schema=,
31
- :reset!, :switch_to_schema, :switch_to_tenant, :with_tenant,
32
- :create_tenant_schema, :drop_tenant_schema, to: :"PgMultitenantSchemas::Context"
33
-
34
- # Delegate tenant resolution to TenantResolver
35
- delegate :extract_subdomain, :find_tenant_by_subdomain, :resolve_tenant_from_request,
36
- to: :"PgMultitenantSchemas::TenantResolver"
41
+ # Get the current tenant object
42
+ #
43
+ # @return [Object, nil] The current tenant or nil if not set
44
+ # @see Context.current_tenant
45
+ attr_accessor :current_tenant
46
+
47
+ # Set the current tenant object
48
+ #
49
+ # @param tenant [Object] The tenant to set as current
50
+ # @see Context.current_tenant=
51
+
52
+ # Get the current tenant schema name
53
+ #
54
+ # @return [String] The current schema name
55
+ # @see Context.current_schema
56
+ attr_accessor :current_schema
57
+
58
+ # Set the current tenant schema name
59
+ #
60
+ # @param schema [String] The schema name to set as current
61
+ # @see Context.current_schema=
62
+
63
+ # Reset the current tenant context
64
+ #
65
+ # @return [void]
66
+ # @see Context.reset!
67
+ def reset!
68
+ Context.reset!
69
+ end
70
+
71
+ # Switch to a specific schema
72
+ #
73
+ # @param schema_name [String] The schema name to switch to
74
+ # @return [void]
75
+ # @see Context.switch_to_schema
76
+ def switch_to_schema(schema_name)
77
+ Context.switch_to_schema(schema_name)
78
+ end
79
+
80
+ # Switch to a specific tenant's schema
81
+ #
82
+ # @param tenant [Object, String] The tenant object or schema name
83
+ # @return [void]
84
+ # @see Context.switch_to_tenant
85
+ def switch_to_tenant(tenant)
86
+ Context.switch_to_tenant(tenant)
87
+ end
88
+
89
+ # Execute a block within a tenant context
90
+ #
91
+ # @param tenant_or_schema [Object, String] The tenant object or schema name
92
+ # @yield Executes the given block in the tenant's schema context
93
+ # @return [Object] The return value of the block
94
+ # @see Context.with_tenant
95
+ def with_tenant(tenant_or_schema, &)
96
+ Context.with_tenant(tenant_or_schema, &)
97
+ end
98
+
99
+ # Create a new tenant schema
100
+ #
101
+ # @param tenant_or_schema [Object, String] The tenant object or schema name
102
+ # @return [void]
103
+ # @see Context.create_tenant_schema
104
+ def create_tenant_schema(tenant_or_schema)
105
+ Context.create_tenant_schema(tenant_or_schema)
106
+ end
107
+
108
+ # Drop a tenant schema
109
+ #
110
+ # @param tenant_or_schema [Object, String] The tenant object or schema name
111
+ # @param cascade [Boolean] Whether to drop dependent objects (default: true)
112
+ # @return [void]
113
+ # @see Context.drop_tenant_schema
114
+ def drop_tenant_schema(tenant_or_schema, cascade: true)
115
+ Context.drop_tenant_schema(tenant_or_schema, cascade: cascade)
116
+ end
117
+
118
+ # Extract tenant subdomain from a hostname
119
+ #
120
+ # @param hostname [String] The hostname to extract from
121
+ # @return [String, nil] The extracted subdomain or nil
122
+ # @see TenantResolver.extract_subdomain
123
+ def extract_subdomain(hostname)
124
+ TenantResolver.extract_subdomain(hostname)
125
+ end
126
+
127
+ # Find a tenant by subdomain
128
+ #
129
+ # @param subdomain [String] The subdomain to search for
130
+ # @return [Object, nil] The tenant object or nil if not found
131
+ # @see TenantResolver.find_tenant_by_subdomain
132
+ def find_tenant_by_subdomain(subdomain)
133
+ TenantResolver.find_tenant_by_subdomain(subdomain)
134
+ end
135
+
136
+ # Resolve tenant from Rails request
137
+ #
138
+ # @param request [ActionDispatch::Request] The Rails request object
139
+ # @return [Object, nil] The tenant object or nil if not resolved
140
+ # @see TenantResolver.resolve_tenant_from_request
141
+ def resolve_tenant_from_request(request)
142
+ TenantResolver.resolve_tenant_from_request(request)
143
+ end
144
+ end
145
+
146
+ # Get the current configuration
147
+ #
148
+ # @return [Configuration] The gem configuration object
149
+ def self.configuration
150
+ @configuration ||= Configuration.new
151
+ end
152
+
153
+ # Configure the gem
154
+ #
155
+ # @yield Yields the configuration object for configuration
156
+ # @example
157
+ # PgMultitenantSchemas.configure do |config|
158
+ # config.tenant_model_class = 'Tenant'
159
+ # config.default_schema = 'public'
160
+ # end
161
+ def self.configure
162
+ yield(configuration) if block_given?
37
163
  end
38
164
  end
@@ -5,22 +5,28 @@ require_relative "lib/pg_multitenant_schemas/version"
5
5
  Gem::Specification.new do |spec|
6
6
  spec.name = "pg_multitenant_schemas"
7
7
  spec.version = PgMultitenantSchemas::VERSION
8
- spec.authors = ["Ruben Paz"]
9
- spec.email = ["rubenpazchuspe@outlook.com"]
8
+ spec.authors = ["L BYTE EIRL"]
9
+ spec.email = ["info@lbyte.io"]
10
10
 
11
- spec.summary = "Modern PostgreSQL schema-based multitenancy for Rails 7+ applications"
12
- spec.description = "A modern Ruby gem that provides PostgreSQL schema-based multitenancy with automatic tenant " \
13
- "resolution and schema switching. Compatible with Rails 7+ and Ruby 3.0+, focusing on security, " \
14
- "performance, and developer experience. Perfect for modern SaaS applications requiring " \
15
- "secure tenant isolation."
11
+ spec.summary = "Modern PostgreSQL schema-based multitenancy for Rails"
12
+ spec.description = "A production-ready Ruby gem providing PostgreSQL " \
13
+ "schema-based multitenancy with automatic tenant resolution, " \
14
+ "secure isolation, and high performance. Built for Rails 8+ and " \
15
+ "Ruby 4.0+, tested with latest stable versions. Features comprehensive " \
16
+ "test coverage (217 tests, 100% passing), complete YARD " \
17
+ "documentation, and interactive HTML documentation site. Perfect " \
18
+ "for modern SaaS applications with database-level isolation."
16
19
  spec.homepage = "https://github.com/rubenpazch/pg_multitenant_schemas"
17
20
  spec.license = "MIT"
18
- spec.required_ruby_version = ">= 3.0.0"
21
+ spec.required_ruby_version = ">= 4.0.0"
19
22
 
20
23
  spec.metadata["allowed_push_host"] = "https://rubygems.org"
21
24
  spec.metadata["homepage_uri"] = spec.homepage
22
25
  spec.metadata["source_code_uri"] = spec.homepage
23
26
  spec.metadata["changelog_uri"] = "#{spec.homepage}/blob/main/CHANGELOG.md"
27
+ spec.metadata["documentation_uri"] = "https://rubenpazch.github.io/pg_multitenant_schemas"
28
+ spec.metadata["bug_tracker_uri"] = "#{spec.homepage}/issues"
29
+ spec.metadata["github_repo"] = "ssh://git@github.com/rubenpazch/pg_multitenant_schemas.git"
24
30
 
25
31
  # Specify which files should be added to the gem when it is released.
26
32
  spec.files = Dir.chdir(__dir__) do
@@ -37,5 +43,5 @@ Gem::Specification.new do |spec|
37
43
  # Runtime dependencies - Rails 7+ support with Rails 8 optimizations
38
44
  spec.add_dependency "activerecord", ">= 7.0", "< 9.0"
39
45
  spec.add_dependency "activesupport", ">= 7.0", "< 9.0"
40
- spec.add_dependency "pg", "~> 1.5"
46
+ spec.add_dependency "pg", "~> 1.6"
41
47
  end