create org model and schema manager
This commit is contained in:
2
Gemfile
2
Gemfile
@@ -49,6 +49,8 @@ group :development, :test do
|
|||||||
|
|
||||||
# Omakase Ruby styling [https://github.com/rails/rubocop-rails-omakase/]
|
# Omakase Ruby styling [https://github.com/rails/rubocop-rails-omakase/]
|
||||||
gem "rubocop-rails-omakase", require: false
|
gem "rubocop-rails-omakase", require: false
|
||||||
|
|
||||||
|
gem 'dotenv-rails'
|
||||||
end
|
end
|
||||||
|
|
||||||
group :development do
|
group :development do
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
class Current < ActiveSupport::CurrentAttributes
|
class Current < ActiveSupport::CurrentAttributes
|
||||||
attribute :session
|
attribute :session
|
||||||
delegate :user, to: :session, allow_nil: true
|
delegate :user, to: :session, allow_nil: true
|
||||||
|
attribute :organization, :user
|
||||||
end
|
end
|
||||||
|
|||||||
27
app/models/organization.rb
Normal file
27
app/models/organization.rb
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Organization < ApplicationRecord
|
||||||
|
validates :name, presence: true
|
||||||
|
validates :subdomain, presence: true, uniqueness: true
|
||||||
|
|
||||||
|
after_create :create_schema
|
||||||
|
before_destroy :drop_schema
|
||||||
|
|
||||||
|
def schema_name
|
||||||
|
"org_#{id.to_s.gsub('-', '_')}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def with_schema(&block)
|
||||||
|
SchemaManager.with_schema(id, &block)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def create_schema
|
||||||
|
SchemaManager.create_schema(id)
|
||||||
|
end
|
||||||
|
|
||||||
|
def drop_schema
|
||||||
|
SchemaManager.drop_schema(id)
|
||||||
|
end
|
||||||
|
end
|
||||||
67
app/models/schema_manager.rb
Normal file
67
app/models/schema_manager.rb
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
# app/models/schema_manager.rb
|
||||||
|
class SchemaManager
|
||||||
|
class << self
|
||||||
|
def create_schema(organization_id)
|
||||||
|
schema_name = schema_name_for(organization_id)
|
||||||
|
|
||||||
|
ActiveRecord::Base.connection.execute("CREATE SCHEMA IF NOT EXISTS #{schema_name}")
|
||||||
|
|
||||||
|
with_schema(organization_id) do
|
||||||
|
# Run all existing migrations in this schema
|
||||||
|
migration_context = ActiveRecord::MigrationContext.new(Rails.application.paths["db/migrate"].first)
|
||||||
|
migration_context.migrate
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def drop_schema(organization_id)
|
||||||
|
schema_name = schema_name_for(organization_id)
|
||||||
|
ActiveRecord::Base.connection.execute("DROP SCHEMA IF EXISTS #{schema_name} CASCADE")
|
||||||
|
end
|
||||||
|
|
||||||
|
def with_schema(organization_id)
|
||||||
|
schema_name = schema_name_for(organization_id)
|
||||||
|
original_schema = ActiveRecord::Base.connection.schema_search_path
|
||||||
|
|
||||||
|
begin
|
||||||
|
ActiveRecord::Base.connection.schema_search_path = schema_name
|
||||||
|
yield
|
||||||
|
ensure
|
||||||
|
ActiveRecord::Base.connection.schema_search_path = original_schema
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def schema_name_for(organization_id)
|
||||||
|
"org_#{organization_id.to_s.gsub('-', '_')}"
|
||||||
|
end
|
||||||
|
|
||||||
|
# Migrate all existing organization schemas
|
||||||
|
def migrate_all_schemas
|
||||||
|
migration_context = ActiveRecord::MigrationContext.new(Rails.application.paths["db/migrate"].first)
|
||||||
|
|
||||||
|
Organization.find_each do |org|
|
||||||
|
puts "Migrating schema for #{org.name}..."
|
||||||
|
org.with_schema do
|
||||||
|
migration_context.migrate
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# List all organization schemas
|
||||||
|
def list_schemas
|
||||||
|
result = ActiveRecord::Base.connection.execute(
|
||||||
|
"SELECT schema_name FROM information_schema.schemata
|
||||||
|
WHERE schema_name LIKE 'org_%'"
|
||||||
|
)
|
||||||
|
result.map { |row| row['schema_name'] }
|
||||||
|
end
|
||||||
|
|
||||||
|
# Check if schema exists
|
||||||
|
def schema_exists?(organization_id)
|
||||||
|
schema_name = schema_name_for(organization_id)
|
||||||
|
result = ActiveRecord::Base.connection.execute(
|
||||||
|
"SELECT EXISTS(SELECT 1 FROM information_schema.schemata WHERE schema_name = '#{schema_name}')"
|
||||||
|
)
|
||||||
|
result.first['exists']
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
21
db/migrate/20250611160914_create_organizations.rb
Normal file
21
db/migrate/20250611160914_create_organizations.rb
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
class CreateOrganizations < ActiveRecord::Migration[8.0]
|
||||||
|
def change
|
||||||
|
# Enable pgcrypto extension for UUID support
|
||||||
|
enable_extension 'pgcrypto' if connection.adapter_name == 'PostgreSQL'
|
||||||
|
|
||||||
|
create_table :organizations, id: :uuid do |t|
|
||||||
|
t.string :name
|
||||||
|
t.string :subdomain
|
||||||
|
t.string :email_domain
|
||||||
|
t.string :logo_url
|
||||||
|
t.string :address
|
||||||
|
t.string :website
|
||||||
|
t.string :phone_number
|
||||||
|
|
||||||
|
t.timestamps
|
||||||
|
end
|
||||||
|
|
||||||
|
add_index :organizations, :subdomain, unique: true
|
||||||
|
add_reference :users, :organization, null: false, foreign_key: true, type: :uuid
|
||||||
|
end
|
||||||
|
end
|
||||||
19
test/fixtures/organizations.yml
vendored
Normal file
19
test/fixtures/organizations.yml
vendored
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
|
||||||
|
|
||||||
|
one:
|
||||||
|
name: MyString
|
||||||
|
subdomain: MyString
|
||||||
|
email_domain: MyString
|
||||||
|
logo_url: MyString
|
||||||
|
address: MyString
|
||||||
|
website: MyString
|
||||||
|
phone_number: MyString
|
||||||
|
|
||||||
|
two:
|
||||||
|
name: MyString
|
||||||
|
subdomain: MyString
|
||||||
|
email_domain: MyString
|
||||||
|
logo_url: MyString
|
||||||
|
address: MyString
|
||||||
|
website: MyString
|
||||||
|
phone_number: MyString
|
||||||
7
test/models/organization_test.rb
Normal file
7
test/models/organization_test.rb
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
require "test_helper"
|
||||||
|
|
||||||
|
class OrganizationTest < ActiveSupport::TestCase
|
||||||
|
# test "the truth" do
|
||||||
|
# assert true
|
||||||
|
# end
|
||||||
|
end
|
||||||
Reference in New Issue
Block a user