create org model and schema manager

This commit is contained in:
2025-06-11 18:23:16 +02:00
parent 16b27a0d1c
commit 8be82f5c49
7 changed files with 144 additions and 0 deletions

View File

@@ -49,6 +49,8 @@ group :development, :test do
# Omakase Ruby styling [https://github.com/rails/rubocop-rails-omakase/]
gem "rubocop-rails-omakase", require: false
gem 'dotenv-rails'
end
group :development do

View File

@@ -1,4 +1,5 @@
class Current < ActiveSupport::CurrentAttributes
attribute :session
delegate :user, to: :session, allow_nil: true
attribute :organization, :user
end

View 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

View 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

View 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
View 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

View File

@@ -0,0 +1,7 @@
require "test_helper"
class OrganizationTest < ActiveSupport::TestCase
# test "the truth" do
# assert true
# end
end