diff --git a/.github/workflows/active-record-multi-tenant-tests.yml b/.github/workflows/active-record-multi-tenant-tests.yml new file mode 100644 index 00000000..9e5ed6c0 --- /dev/null +++ b/.github/workflows/active-record-multi-tenant-tests.yml @@ -0,0 +1,83 @@ +name: Active Record Multi-Tenant Tests + +env: + CI: true +on: + push: + branches: + - "**" + pull_request: + types: [ opened, reopened, synchronize ] + +jobs: + + static-checks: + runs-on: ubuntu-latest + steps: + - uses: ruby/setup-ruby@v1 + with: + ruby-version: 3.2 + bundler-cache: true + - uses: actions/checkout@v3 + - name: Rubocop static code analysis + run: | + gem install rubocop + rubocop + doc_checks: + runs-on: ubuntu-latest + steps: + - uses: actions/setup-python@v4 + with: + python-version: 3.9 + - uses: actions/checkout@v3 + - name: Install python dependencies + run: | + pip install -r docs/requirements.txt + - name: Documentation Checks + run: | + cd docs + sphinx-build -W -b html source builds + + build: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + ruby: + - '3.0' + - '3.1' + - '3.2' + appraisal: + - rails-6.0 + - rails-6.1 + - rails-7.0 + - rails-7.1 + - active-record-6.0 + - active-record-6.1 + - active-record-7.0 + - active-record-7.1 + citus_version: + - '10' + - '11' + - '12' + + name: Ruby ${{ matrix.ruby }}/${{ matrix.gemfile }} / Citus ${{ matrix.citus_version }} + env: + APPRAISAL: ${{ matrix.appraisal }} + CITUS_VERSION: ${{ matrix.citus_version }} + steps: + - uses: actions/checkout@v3 + + - name: Start Citus Database environment + run: docker-compose up -d + + - uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ matrix.ruby }} + bundler-cache: true + + - name: Execute tests + run: bundle exec rake spec + + - name: Upload coverage reports to Codecov + uses: codecov/codecov-action@v3 diff --git a/.gitignore b/.gitignore index d8f08963..b600ebfc 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,11 @@ spec/debug.log pkg/ *.rb# *.*~ +Gemfile.lock +*.gemfile.lock +.idea/ +.vagrant/ +Vagrantfile +coverage/ +docs/build/ +.yardoc/ \ No newline at end of file diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 00000000..688e898b --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,15 @@ +version: 2 + +build: + os: "ubuntu-22.04" + tools: + python: "3.11" + +# Build from the docs/ directory with Sphinx +sphinx: + configuration: docs/source/conf.py + +# Explicitly set the version of Python and its requirements +python: + install: + - requirements: docs/requirements.txt diff --git a/.rspec b/.rspec new file mode 100644 index 00000000..18ed8e15 --- /dev/null +++ b/.rspec @@ -0,0 +1 @@ +--force-color diff --git a/.rubocop.yml b/.rubocop.yml new file mode 100644 index 00000000..cb334d8b --- /dev/null +++ b/.rubocop.yml @@ -0,0 +1,59 @@ +# This is an example RuboCop configuration file with some commonly used options. + +# Run RuboCop on all Ruby files, except those in `vendor` and `node_modules` directories +AllCops: + Exclude: + - 'vendor/**/*' + - 'node_modules/**/*' + - 'Vagrantfile' + TargetRubyVersion: 3.0 + SuggestExtensions: false + NewCops: enable + +Gemspec/DevelopmentDependencies: + Enabled: false + +Lint/ConstantDefinitionInBlock: + Enabled: false + +Lint/EmptyBlock: + Enabled: false + +Style/ClassAndModuleChildren: + Enabled: false + +Style/Documentation: + Exclude: + - '**/*.rb' + Enabled: false + +Style/DocumentDynamicEvalDefinition: + Enabled: false + +Metrics/BlockLength: + Max: 650 + +Metrics/MethodLength: + Max: 150 + +Metrics/ClassLength: + Max: 200 + +Metrics/ModuleLength: + Max: 200 + +Metrics/AbcSize: + Enabled: false + +Metrics/PerceivedComplexity: + Enabled: false + +Metrics/CyclomaticComplexity: + Enabled: false + +Metrics/BlockNesting: + Enabled: false + +Naming/FileName: + Exclude: + - 'lib/activerecord-multi-tenant.rb' diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 6e5bd5e9..00000000 --- a/.travis.yml +++ /dev/null @@ -1,34 +0,0 @@ -sudo: required -cache: bundler - -language: ruby - -rvm: - - 2.5.8 - - 2.6.4 - - 2.7.1 - -gemfile: - - gemfiles/rails_5.2.gemfile - - gemfiles/rails_6.0.gemfile - - gemfiles/rails_6.1.gemfile - - gemfiles/active_record_5.2.gemfile - - gemfiles/active_record_6.0.gemfile - - gemfiles/active_record_6.1.gemfile - -env: - - PREPARED_STATEMENTS=0 - - PREPARED_STATEMENTS=1 - -matrix: - fast_finish: true - -services: - - docker - -before_install: - - docker-compose up -d - - gem install bundler -v 2.1.4 - -script: - - bundle exec rake spec diff --git a/Appraisals b/Appraisals index 213c64d7..7b1f9086 100644 --- a/Appraisals +++ b/Appraisals @@ -1,14 +1,4 @@ -appraise 'rails-5.2' do - gem 'rails', '~> 5.2.0' - gem 'i18n', '~> 0.9.5' - gem 'nokogiri', '~> 1.7.1' - gem 'nio4r', '~> 2.3.1' - gem 'sprockets', '~> 3.7.1' - gem 'byebug', '~> 11.0' - gem 'rake', '12.0.0' - gem 'redis', '3.3.3' - gem 'pry-byebug', '3.9.0' -end +# frozen_string_literal: true appraise 'rails-6.0' do gem 'rails', '~> 6.0.3' @@ -18,16 +8,12 @@ appraise 'rails-6.1' do gem 'rails', '~> 6.1.0' end -appraise 'active-record-5.2' do - gem 'activerecord', '~> 5.2.0' - gem 'i18n', '~> 0.9.5' - gem 'nokogiri', '~> 1.7.1' - gem 'nio4r', '~> 2.3.1' - gem 'sprockets', '~> 3.7.1' - gem 'byebug', '~> 11.0' - gem 'rake', '12.0.0' - gem 'redis', '3.3.3' - gem 'pry-byebug', '3.9.0' +appraise 'rails-7.0' do + gem 'rails', '~> 7.0.0' +end + +appraise 'rails-7.1' do + gem 'rails', '~> 7.1.0' end appraise 'active-record-6.0' do @@ -37,3 +23,11 @@ end appraise 'active-record-6.1' do gem 'activerecord', '~> 6.1.0' end + +appraise 'active-record-7.0' do + gem 'activerecord', '~> 7.0.0' +end + +appraise 'active-record-7.1' do + gem 'activerecord', '~> 7.1.0' +end diff --git a/CHANGELOG.md b/CHANGELOG.md index 045d1b6a..bb95949d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,64 @@ # Changelog +## 2.4.0 2023-09-22 +* Adds citus 12 to test matrix (#210) +* Adds Support for rails 7.1 (#208) +* Fix missing scope in habtm.rb (#207) +* Update logic inside the tenant_klass_defined? method (#202) + +## 2.3.0 2023-06-05 +* Adds has_and_belongs_to_many feature with tenant (#193) +* Removes eol ruby versions +* Adds documentation in ReadTheDocs platform (#196) +* Organizes badges in documentation and README.md (#197) +* Wrap ActiveRecord::Base with ActiveSupport.on_load (#199) + +## 2.2.0 2022-12-06 +* Handle changing tenant from `nil` to a value [#173](https://github.com/citusdata/activerecord-multi-tenant/pull/173) +* Allow Partitioned tables to be created without a primary key [#172](https://github.com/citusdata/activerecord-multi-tenant/pull/172) +* Only attempt to reload with MultiTenant when parition_key is present [#175](https://github.com/citusdata/activerecord-multi-tenant/pull/175) +* Remove support for Ruby 2.5 & ActiveRecord 5.2 + +## 2.1.6 2022-11-23 +* Fix undefined wrap_methods error & wrap_methods version check [#170](https://github.com/citusdata/activerecord-multi-tenant/pull/170) + +## 2.1.5 2022-11-20 +* Fix `MultiTenant.without` codegen bug in Rails 6.1+ [#168](https://github.com/citusdata/activerecord-multi-tenant/pull/168) + +## 2.1.4 2022-11-03 +* Fixes #166 where db:schema:dump is broken when using this gem with MySQL [#167](https://github.com/citusdata/activerecord-multi-tenant/pull/167) + +## 2.1.3 2022-10-27 +* Error when calling a method that takes keyword arguments with MultiTenant.wrap_methods [#164](https://github.com/citusdata/activerecord-multi-tenant/pull/164) + +## 2.1.2 2022-10-26 +* Fixes issue when wraping methods that require a block [#162](https://github.com/citusdata/activerecord-multi-tenant/pull/162) + +## 2.1.1 2022-10-20 +* Fix query building for models with mismatched partition_keys [#150](https://github.com/citusdata/activerecord-multi-tenant/pull/150) +* Identify tenant even if class name is nonstandard [#152](https://github.com/citusdata/activerecord-multi-tenant/pull/152) +* Add current_tenant_id to WHERE clauses when calling methods on activerecord instance or its associations [#154](https://github.com/citusdata/activerecord-multi-tenant/pull/154) +* Make create_distributed_table, create_reference_table reversible & add ruby wrapper for rebalance_table_shards [#155](https://github.com/citusdata/activerecord-multi-tenant/pull/155) +* Support create_distributed_table, create_reference_table in schema.rb [#156](https://github.com/citusdata/activerecord-multi-tenant/pull/156) +* Add client and server sidekiq middleware to sidekiq middleware chain [#158](https://github.com/citusdata/activerecord-multi-tenant/pull/158) + +## 2.0.0 2022-05-19 + +* Replace RequestStore with CurrentAttributes [#139](https://github.com/citusdata/activerecord-multi-tenant/pull/139) +* Support changing table_name after calling multi_tenant [#128](https://github.com/citusdata/activerecord-multi-tenant/pull/128) +* Allow to use uuid as primary key on partition table [#112](https://github.com/citusdata/activerecord-multi-tenant/pull/112) +* Support latest Rails 5.2 [#145](https://github.com/citusdata/activerecord-multi-tenant/pull/145) +* Support optional: true for belongs_to [#147](https://github.com/citusdata/activerecord-multi-tenant/pull/147) + + +## 1.2.0 2022-03-29 + +* Test Rails 7 & Ruby 3 +* Fix regression in 1.1.1 involving deleted tenants [#123](https://github.com/citusdata/activerecord-multi-tenant/pull/123) +* Fix incorrect SQL generated when joining two models and one has a default scope [#132](https://github.com/citusdata/activerecord-multi-tenant/pull/132) +* Update for Rails 5+ removal of type_cast_for_database [#135](https://github.com/citusdata/activerecord-multi-tenant/pull/135) + + ## 1.1.1 2021-01-15 * Add support for Rails 6.1 [#108](https://github.com/citusdata/activerecord-multi-tenant/pull/108) diff --git a/Gemfile b/Gemfile index e37a382a..3ad7db9f 100644 --- a/Gemfile +++ b/Gemfile @@ -1,5 +1,9 @@ +# frozen_string_literal: true + source 'https://rubygems.org' gemspec - gem 'appraisal' +gem 'rubocop', require: false, group: 'test' +gem 'simplecov' +gem 'simplecov-cobertura' diff --git a/Gemfile.lock b/Gemfile.lock deleted file mode 100644 index b4f35e05..00000000 --- a/Gemfile.lock +++ /dev/null @@ -1,199 +0,0 @@ -PATH - remote: . - specs: - activerecord-multi-tenant (1.1.1) - rails (>= 4.2) - request_store (>= 1.0.5) - -GEM - remote: https://rubygems.org/ - specs: - actioncable (6.0.3.3) - actionpack (= 6.0.3.3) - nio4r (~> 2.0) - websocket-driver (>= 0.6.1) - actionmailbox (6.0.3.3) - actionpack (= 6.0.3.3) - activejob (= 6.0.3.3) - activerecord (= 6.0.3.3) - activestorage (= 6.0.3.3) - activesupport (= 6.0.3.3) - mail (>= 2.7.1) - actionmailer (6.0.3.3) - actionpack (= 6.0.3.3) - actionview (= 6.0.3.3) - activejob (= 6.0.3.3) - mail (~> 2.5, >= 2.5.4) - rails-dom-testing (~> 2.0) - actionpack (6.0.3.3) - actionview (= 6.0.3.3) - activesupport (= 6.0.3.3) - rack (~> 2.0, >= 2.0.8) - rack-test (>= 0.6.3) - rails-dom-testing (~> 2.0) - rails-html-sanitizer (~> 1.0, >= 1.2.0) - actiontext (6.0.3.3) - actionpack (= 6.0.3.3) - activerecord (= 6.0.3.3) - activestorage (= 6.0.3.3) - activesupport (= 6.0.3.3) - nokogiri (>= 1.8.5) - actionview (6.0.3.3) - activesupport (= 6.0.3.3) - builder (~> 3.1) - erubi (~> 1.4) - rails-dom-testing (~> 2.0) - rails-html-sanitizer (~> 1.1, >= 1.2.0) - activejob (6.0.3.3) - activesupport (= 6.0.3.3) - globalid (>= 0.3.6) - activemodel (6.0.3.3) - activesupport (= 6.0.3.3) - activerecord (6.0.3.3) - activemodel (= 6.0.3.3) - activesupport (= 6.0.3.3) - activestorage (6.0.3.3) - actionpack (= 6.0.3.3) - activejob (= 6.0.3.3) - activerecord (= 6.0.3.3) - marcel (~> 0.3.1) - activesupport (6.0.3.3) - concurrent-ruby (~> 1.0, >= 1.0.2) - i18n (>= 0.7, < 2) - minitest (~> 5.1) - tzinfo (~> 1.1) - zeitwerk (~> 2.2, >= 2.2.2) - appraisal (2.2.0) - bundler - rake - thor (>= 0.14.0) - builder (3.2.4) - byebug (11.0.1) - coderay (1.1.2) - concurrent-ruby (1.1.7) - connection_pool (2.2.2) - crass (1.0.6) - diff-lcs (1.3) - erubi (1.9.0) - globalid (0.4.2) - activesupport (>= 4.2.0) - i18n (1.8.5) - concurrent-ruby (~> 1.0) - loofah (2.7.0) - crass (~> 1.0.2) - nokogiri (>= 1.5.9) - mail (2.7.1) - mini_mime (>= 0.1.1) - marcel (0.3.3) - mimemagic (~> 0.3.2) - method_source (0.9.2) - mimemagic (0.3.5) - mini_mime (1.0.2) - mini_portile2 (2.5.0) - minitest (5.14.2) - nio4r (2.5.4) - nokogiri (1.11.1) - mini_portile2 (~> 2.5.0) - racc (~> 1.4) - pg (1.1.4) - pry (0.12.2) - coderay (~> 1.1.0) - method_source (~> 0.9.0) - pry-byebug (3.7.0) - byebug (~> 11.0) - pry (~> 0.10) - racc (1.5.2) - rack (2.2.3) - rack-protection (2.0.5) - rack - rack-test (1.1.0) - rack (>= 1.0, < 3) - rails (6.0.3.3) - actioncable (= 6.0.3.3) - actionmailbox (= 6.0.3.3) - actionmailer (= 6.0.3.3) - actionpack (= 6.0.3.3) - actiontext (= 6.0.3.3) - actionview (= 6.0.3.3) - activejob (= 6.0.3.3) - activemodel (= 6.0.3.3) - activerecord (= 6.0.3.3) - activestorage (= 6.0.3.3) - activesupport (= 6.0.3.3) - bundler (>= 1.3.0) - railties (= 6.0.3.3) - sprockets-rails (>= 2.0.0) - rails-dom-testing (2.0.3) - activesupport (>= 4.2.0) - nokogiri (>= 1.6) - rails-html-sanitizer (1.3.0) - loofah (~> 2.3) - railties (6.0.3.3) - actionpack (= 6.0.3.3) - activesupport (= 6.0.3.3) - method_source - rake (>= 0.8.7) - thor (>= 0.20.3, < 2.0) - rake (13.0.1) - redis (4.1.2) - request_store (1.5.0) - rack (>= 1.4) - rspec (3.8.0) - rspec-core (~> 3.8.0) - rspec-expectations (~> 3.8.0) - rspec-mocks (~> 3.8.0) - rspec-core (3.8.2) - rspec-support (~> 3.8.0) - rspec-expectations (3.8.4) - diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.8.0) - rspec-mocks (3.8.1) - diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.8.0) - rspec-rails (3.8.2) - actionpack (>= 3.0) - activesupport (>= 3.0) - railties (>= 3.0) - rspec-core (~> 3.8.0) - rspec-expectations (~> 3.8.0) - rspec-mocks (~> 3.8.0) - rspec-support (~> 3.8.0) - rspec-support (3.8.2) - sidekiq (5.2.7) - connection_pool (~> 2.2, >= 2.2.2) - rack (>= 1.5.0) - rack-protection (>= 1.5.0) - redis (>= 3.3.5, < 5) - sprockets (4.0.2) - concurrent-ruby (~> 1.0) - rack (> 1, < 3) - sprockets-rails (3.2.2) - actionpack (>= 4.0) - activesupport (>= 4.0) - sprockets (>= 3.0.0) - thor (0.20.3) - thread_safe (0.3.6) - tzinfo (1.2.7) - thread_safe (~> 0.1) - websocket-driver (0.7.3) - websocket-extensions (>= 0.1.0) - websocket-extensions (0.1.5) - zeitwerk (2.4.0) - -PLATFORMS - ruby - -DEPENDENCIES - activerecord-multi-tenant! - appraisal - pg - pry - pry-byebug - rake - rspec (>= 3.0) - rspec-rails - sidekiq - thor - -BUNDLED WITH - 2.1.4 diff --git a/README.md b/README.md index cd9cbae1..34e97d8b 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ -# activerecord-multi-tenant [ ![](https://img.shields.io/gem/v/activerecord-multi-tenant.svg)](https://rubygems.org/gems/activerecord-multi-tenant) [ ![](https://img.shields.io/gem/dt/activerecord-multi-tenant.svg)](https://rubygems.org/gems/activerecord-multi-tenant) +# activerecord-multi-tenant +[![Build Status](https://github.com/citusdata/activerecord-multi-tenant/actions/workflows/active-record-multi-tenant-tests.yml/badge.svg)](https://github.com/citusdata/activerecord-multi-tenant/actions/workflows/active-record-multi-tenant-tests.yml) [![codecov](https://codecov.io/gh/citusdata/activerecord-multi-tenant/branch/master/graph/badge.svg?token=rw0TsEk4Ld)](https://codecov.io/gh/citusdata/activerecord-multi-tenant) [ ![Gems Version](https://img.shields.io/gem/v/activerecord-multi-tenant.svg)](https://rubygems.org/gems/activerecord-multi-tenant)[ ![Gem Download Count](https://img.shields.io/gem/dt/activerecord-multi-tenant.svg)](https://rubygems.org/gems/activerecord-multi-tenant) [![Documentation Status](https://readthedocs.org/projects/activerecord-multi-tenant/badge/?version=latest)](https://activerecord-multi-tenant.readthedocs.io/en/latest/?badge=latest) Introduction Post: https://www.citusdata.com/blog/2017/01/05/easily-scale-out-multi-tenant-apps/ @@ -16,7 +17,7 @@ gem 'activerecord-multi-tenant' ## Supported Rails versions -All Ruby on Rails versions starting with 4.2 or newer (up to 6.0) are supported. +All Ruby on Rails versions starting with 6.0 or newer (up to 7.0) are supported. This gem only supports ActiveRecord (the Rails default ORM), and not alternative ORMs like Sequel. diff --git a/Rakefile b/Rakefile index 65c7261d..e5ff0a2a 100644 --- a/Rakefile +++ b/Rakefile @@ -1,7 +1,9 @@ +# frozen_string_literal: true + require 'rubygems' require 'bundler/setup' require 'bundler/gem_tasks' require 'rspec/core/rake_task' RSpec::Core::RakeTask.new(:spec) -task :default => :spec +task default: :spec diff --git a/activerecord-multi-tenant.gemspec b/activerecord-multi-tenant.gemspec index 0a0bf9c8..0811991c 100644 --- a/activerecord-multi-tenant.gemspec +++ b/activerecord-multi-tenant.gemspec @@ -1,29 +1,36 @@ -$:.push File.expand_path('../lib', __FILE__) +# frozen_string_literal: true + +$LOAD_PATH.push File.expand_path('lib', __dir__) require 'activerecord-multi-tenant/version' -Gem::Specification.new do |s| - s.name = 'activerecord-multi-tenant' - s.version = MultiTenant::VERSION - s.summary = 'ActiveRecord/Rails integration for multi-tenant databases, in particular the Citus extension for PostgreSQL' - s.description = '' - s.authors = ['Citus Data'] - s.email = 'engage@citusdata.com' +Gem::Specification.new do |spec| + spec.name = 'activerecord-multi-tenant' + spec.version = MultiTenant::VERSION + spec.summary = 'ActiveRecord/Rails integration for multi-tenant databases, ' \ + 'in particular the Citus extension for PostgreSQL' + spec.description = '' + spec.authors = ['Citus Data'] + spec.email = 'engage@citusdata.com' + spec.required_ruby_version = '>= 3.0.0' + spec.metadata = { 'rubygems_mfa_required' => 'true' } + + spec.files = `git ls-files`.split("\n") + spec.require_paths = ['lib'] + spec.homepage = 'https://github.com/citusdata/activerecord-multi-tenant' + spec.license = 'MIT' - s.files = `git ls-files`.split("\n") - s.test_files = `git ls-files -- {spec}/*`.split("\n") - s.require_paths = ['lib'] - s.homepage = 'https://github.com/citusdata/activerecord-multi-tenant' - s.license = 'MIT' + spec.add_dependency 'rails', '>= 6' - s.add_runtime_dependency('request_store', '>= 1.0.5') - s.add_dependency('rails','>= 4.2') + spec.add_development_dependency 'anbt-sql-formatter' + spec.add_development_dependency 'codecov' + spec.add_development_dependency 'pg' + spec.add_development_dependency 'pry' + spec.add_development_dependency 'pry-byebug' + spec.add_development_dependency 'rake' + spec.add_development_dependency 'rspec', '>= 3.0' + spec.add_development_dependency 'rspec-rails' + spec.add_development_dependency 'rubocop' + spec.add_development_dependency 'sidekiq' - s.add_development_dependency 'rspec', '>= 3.0' - s.add_development_dependency 'rspec-rails' - s.add_development_dependency 'pg' - s.add_development_dependency 'rake' - s.add_development_dependency 'sidekiq' - s.add_development_dependency 'thor' - s.add_development_dependency 'pry' - s.add_development_dependency 'pry-byebug' + spec.add_development_dependency 'thor' end diff --git a/docker-compose.yml b/docker-compose.yml index 696e8064..84260ca7 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,25 +2,31 @@ version: '2.1' services: master: - image: 'citusdata/citus:7.5.1' - ports: ['5600:5432'] - labels: ['com.citusdata.role=Master'] - volumes: ['/var/run/postgresql'] - manager: - container_name: "${COMPOSE_PROJECT_NAME:-citus}_manager" - image: 'citusdata/membership-manager:0.1.0' - volumes: ['/var/run/docker.sock:/var/run/docker.sock'] - depends_on: { master: { condition: service_healthy } } + container_name: "${COMPOSE_PROJECT_NAME:-citus}_master" + image: 'citusdata/citus:${CITUS_VERSION:-11.2}' + ports: [ '5600:5432' ] + labels: [ 'com.citusdata.role=Master' ] + environment: + - POSTGRES_HOST_AUTH_METHOD=trust + command: -c fsync=off -c full_page_writes=off worker1: - image: 'citusdata/citus:7.5.1' - labels: ['com.citusdata.role=Worker'] + image: 'citusdata/citus:${CITUS_VERSION:-11.2}' + ports: [ '5601:5432' ] + labels: [ 'com.citusdata.role=Worker' ] depends_on: { manager: { condition: service_healthy } } + environment: + - POSTGRES_HOST_AUTH_METHOD=trust + command: -c fsync=off -c full_page_writes=off worker2: - image: 'citusdata/citus:7.5.1' - labels: ['com.citusdata.role=Worker'] + image: 'citusdata/citus:${CITUS_VERSION:-11.2}' + ports: [ '5602:5432' ] + labels: [ 'com.citusdata.role=Worker' ] depends_on: { manager: { condition: service_healthy } } - healthcheck: - image: busybox - depends_on: - worker1: { condition: service_healthy } - worker2: { condition: service_healthy } + environment: + - POSTGRES_HOST_AUTH_METHOD=trust + command: -c fsync=off -c full_page_writes=off + manager: + container_name: "${COMPOSE_PROJECT_NAME:-citus}_manager" + image: 'citusdata/membership-manager:0.2.0' + volumes: [ '/var/run/docker.sock:/var/run/docker.sock' ] + depends_on: { master: { condition: service_healthy } } diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 00000000..394d0449 --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1,3 @@ +# Ignore everything in this directory +.yardoc/* + diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 00000000..a2e58bba --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,28 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = source +BUILDDIR = build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# New target that executes the bash script before calling the original target +build-docs: pre-build # Change 'html' to the target you want to execute + +pre-build: + # Execute your bash script here + bash api-reference.sh + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +html: + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + diff --git a/docs/api-reference.sh b/docs/api-reference.sh new file mode 100644 index 00000000..1be5a3b4 --- /dev/null +++ b/docs/api-reference.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +# This script is used to generate the API reference documentation for the +# project. It is intended to be run from the root of the project directory. +# It requires the following tools to be installed: +# - yard (gem install yard) +echo "Generating API reference documentation..." +echo "Pwd: $(pwd)" +cd .. +yard doc --output-dir docs/source/_static/api-reference diff --git a/docs/requirements.in b/docs/requirements.in new file mode 100644 index 00000000..da3e99a3 --- /dev/null +++ b/docs/requirements.in @@ -0,0 +1,4 @@ +sphinxnotes-strike +sphinx +sphinx_rtd_theme +readthedocs-sphinx-search \ No newline at end of file diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 00000000..37de70e5 --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1,62 @@ +# +# This file is autogenerated by pip-compile with Python 3.10 +# by the following command: +# +# pip-compile requirements.in +# +alabaster==0.7.13 + # via sphinx +babel==2.12.1 + # via sphinx +certifi==2023.7.22 + # via requests +charset-normalizer==3.1.0 + # via requests +docutils==0.18.1 + # via + # sphinx + # sphinx-rtd-theme +idna==3.4 + # via requests +imagesize==1.4.1 + # via sphinx +jinja2==3.1.2 + # via sphinx +markupsafe==2.1.2 + # via jinja2 +packaging==23.1 + # via sphinx +pygments==2.15.1 + # via sphinx +readthedocs-sphinx-search==0.3.1 + # via -r requirements.in +requests==2.31.0 + # via sphinx +snowballstemmer==2.2.0 + # via sphinx +sphinx==6.2.1 + # via + # -r requirements.in + # sphinx-rtd-theme + # sphinxcontrib-jquery + # sphinxnotes-strike +sphinx-rtd-theme==1.2.1 + # via -r requirements.in +sphinxcontrib-applehelp==1.0.4 + # via sphinx +sphinxcontrib-devhelp==1.0.2 + # via sphinx +sphinxcontrib-htmlhelp==2.0.1 + # via sphinx +sphinxcontrib-jquery==4.1 + # via sphinx-rtd-theme +sphinxcontrib-jsmath==1.0.1 + # via sphinx +sphinxcontrib-qthelp==1.0.3 + # via sphinx +sphinxcontrib-serializinghtml==1.1.5 + # via sphinx +sphinxnotes-strike==1.2 + # via -r requirements.in +urllib3==2.0.7 + # via requests diff --git a/docs/source/_static/api-reference/ActiveRecord.html b/docs/source/_static/api-reference/ActiveRecord.html new file mode 100644 index 00000000..2e8e5584 --- /dev/null +++ b/docs/source/_static/api-reference/ActiveRecord.html @@ -0,0 +1,130 @@ + + + + + + + Module: ActiveRecord + + — Documentation by YARD 0.9.34 + + + + + + + + + + + + + + + + + + + +
+ + +

Module: ActiveRecord + + + +

+
+ + + + + + + + + + + +
+
Defined in:
+
lib/activerecord-multi-tenant/habtm.rb,
+ lib/activerecord-multi-tenant/query_rewriter.rb,
lib/activerecord-multi-tenant/migrations.rb,
lib/activerecord-multi-tenant/migrations.rb
+
+
+ +
+ +

Overview

+
+ +

This module extension is a monkey patch to the ActiveRecord::Associations::ClassMethods module. It overrides the has_and_belongs_to_many method to add the tenant_id to the join table if the tenant_enabled option is set to true.

+ + +
+
+
+ + +

Defined Under Namespace

+

+ + + Modules: Associations, ConnectionAdapters, QueryMethods + + + + Classes: SchemaDumper + + +

+ + + + + + + + + +
+ + + +
+ + \ No newline at end of file diff --git a/docs/source/_static/api-reference/ActiveRecord/Associations.html b/docs/source/_static/api-reference/ActiveRecord/Associations.html new file mode 100644 index 00000000..018e5f71 --- /dev/null +++ b/docs/source/_static/api-reference/ActiveRecord/Associations.html @@ -0,0 +1,117 @@ + + + + + + + Module: ActiveRecord::Associations + + — Documentation by YARD 0.9.34 + + + + + + + + + + + + + + + + + + + +
+ + +

Module: ActiveRecord::Associations + + + +

+
+ + + + + + + + + + + +
+
Defined in:
+
lib/activerecord-multi-tenant/habtm.rb
+
+ +
+ +

Defined Under Namespace

+

+ + + Modules: ClassMethods + + + + Classes: Association + + +

+ + + + + + + + + +
+ + + +
+ + \ No newline at end of file diff --git a/docs/source/_static/api-reference/ActiveRecord/Associations/Association.html b/docs/source/_static/api-reference/ActiveRecord/Associations/Association.html new file mode 100644 index 00000000..8818d1e5 --- /dev/null +++ b/docs/source/_static/api-reference/ActiveRecord/Associations/Association.html @@ -0,0 +1,285 @@ + + + + + + + Class: ActiveRecord::Associations::Association + + — Documentation by YARD 0.9.34 + + + + + + + + + + + + + + + + + + + +
+ + +

Class: ActiveRecord::Associations::Association + + + +

+
+ +
+
Inherits:
+
+ Object + +
    +
  • Object
  • + + + +
+ show all + +
+
+ + + + + + + + + + + +
+
Defined in:
+
lib/activerecord-multi-tenant/model_extensions.rb
+
+ +
+ +

Overview

+
+ +

skips statement caching for classes that is Multi-tenant or has a multi-tenant relation

+ + +
+
+
+ + +
+ + + + + + + +

+ Instance Method Summary + collapse +

+ + + + + + +
+

Instance Method Details

+ + +
+

+ + #skip_statement_cache?(*scope) ⇒ Boolean + + + + + +

+
+ + +
+
+
+ +

Returns:

+
    + +
  • + + + (Boolean) + + + +
  • + +
+ +
+ + + + +
+
+
+
+194
+195
+196
+197
+198
+199
+200
+201
+202
+203
+
+
# File 'lib/activerecord-multi-tenant/model_extensions.rb', line 194
+
+def skip_statement_cache?(*scope)
+  return true if klass.respond_to?(:scoped_by_tenant?) && klass.scoped_by_tenant?
+
+  if reflection.through_reflection
+    through_klass = reflection.through_reflection.klass
+    return true if through_klass.respond_to?(:scoped_by_tenant?) && through_klass.scoped_by_tenant?
+  end
+
+  skip_statement_cache_orig(*scope)
+end
+
+
+ +
+

+ + #skip_statement_cache_origObject + + + + + +

+ + + + +
+
+
+
+192
+
+
# File 'lib/activerecord-multi-tenant/model_extensions.rb', line 192
+
+alias skip_statement_cache_orig skip_statement_cache?
+
+
+ +
+ +
+ + + +
+ + \ No newline at end of file diff --git a/docs/source/_static/api-reference/ActiveRecord/Associations/ClassMethods.html b/docs/source/_static/api-reference/ActiveRecord/Associations/ClassMethods.html new file mode 100644 index 00000000..2d1785ed --- /dev/null +++ b/docs/source/_static/api-reference/ActiveRecord/Associations/ClassMethods.html @@ -0,0 +1,255 @@ + + + + + + + Module: ActiveRecord::Associations::ClassMethods + + — Documentation by YARD 0.9.34 + + + + + + + + + + + + + + + + + + + +
+ + +

Module: ActiveRecord::Associations::ClassMethods + + + +

+
+ + + + + + + + + + + +
+
Defined in:
+
lib/activerecord-multi-tenant/habtm.rb
+
+ +
+ + + + + + + + + +

+ Instance Method Summary + collapse +

+ + + + + + +
+

Instance Method Details

+ + +
+

+ + #has_and_belongs_to_many_with_tenant(name, options = {}, &extension) ⇒ Object + + + + Also known as: + has_and_belongs_to_many + + + + +

+
+ +

rubocop:disable Naming/PredicateName

+ + +
+
+
+ + +
+ + + + +
+
+
+
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+35
+36
+37
+38
+39
+40
+41
+42
+43
+44
+
+
# File 'lib/activerecord-multi-tenant/habtm.rb', line 11
+
+def has_and_belongs_to_many_with_tenant(name, options = {}, &extension)
+  # rubocop:enable Naming/PredicateName
+  has_and_belongs_to_many_without_tenant(name, **options, &extension)
+
+  middle_reflection = _reflections[name.to_s].through_reflection
+  join_model = middle_reflection.klass
+
+  # get tenant_enabled from options and if it is not set, set it to false
+  tenant_enabled = options[:tenant_enabled] || false
+
+  return unless tenant_enabled
+
+  tenant_class_name = options[:tenant_class_name]
+  tenant_column = options[:tenant_column]
+
+  match = tenant_column.match(/(\w+)_id/)
+  tenant_field_name = match ? match[1] : 'tenant'
+
+  join_model.class_eval do
+    belongs_to tenant_field_name.to_sym, class_name: tenant_class_name
+    before_create :tenant_set
+
+    private
+
+    # This method sets the tenant_id on the join table and executes before creation of the join table record.
+    define_method :tenant_set do
+      if tenant_enabled
+        raise MultiTenant::MissingTenantError, 'Tenant Id is not set' unless MultiTenant.current_tenant_id
+
+        send("#{tenant_column}=", MultiTenant.current_tenant_id)
+      end
+    end
+  end
+end
+
+
+ +
+ +
+ + + +
+ + \ No newline at end of file diff --git a/docs/source/_static/api-reference/ActiveRecord/ConnectionAdapters.html b/docs/source/_static/api-reference/ActiveRecord/ConnectionAdapters.html new file mode 100644 index 00000000..25a67f99 --- /dev/null +++ b/docs/source/_static/api-reference/ActiveRecord/ConnectionAdapters.html @@ -0,0 +1,126 @@ + + + + + + + Module: ActiveRecord::ConnectionAdapters + + — Documentation by YARD 0.9.34 + + + + + + + + + + + + + + + + + + + +
+ + +

Module: ActiveRecord::ConnectionAdapters + + + +

+
+ + + + + + + + + + + +
+
Defined in:
+
lib/activerecord-multi-tenant/migrations.rb
+
+ +
+ +

Overview

+
+ +

:nodoc:

+ + +
+
+
+ + +

Defined Under Namespace

+

+ + + Modules: SchemaStatements + + + + +

+ + + + + + + + + +
+ + + +
+ + \ No newline at end of file diff --git a/docs/source/_static/api-reference/ActiveRecord/ConnectionAdapters/SchemaStatements.html b/docs/source/_static/api-reference/ActiveRecord/ConnectionAdapters/SchemaStatements.html new file mode 100644 index 00000000..5d6f36e8 --- /dev/null +++ b/docs/source/_static/api-reference/ActiveRecord/ConnectionAdapters/SchemaStatements.html @@ -0,0 +1,232 @@ + + + + + + + Module: ActiveRecord::ConnectionAdapters::SchemaStatements + + — Documentation by YARD 0.9.34 + + + + + + + + + + + + + + + + + + + +
+ + +

Module: ActiveRecord::ConnectionAdapters::SchemaStatements + + + +

+
+ + + + + + + + + + + +
+
Defined in:
+
lib/activerecord-multi-tenant/migrations.rb
+
+ +
+ + + + + + + + + +

+ Instance Method Summary + collapse +

+ + + + + + +
+

Instance Method Details

+ + +
+

+ + #create_table(table_name, options = {}, &block) ⇒ Object + + + + + +

+ + + + +
+
+
+
+74
+75
+76
+77
+78
+79
+80
+81
+
+
# File 'lib/activerecord-multi-tenant/migrations.rb', line 74
+
+def create_table(table_name, options = {}, &block)
+  ret = orig_create_table(table_name, **options.except(:partition_key), &block)
+  if options[:id] != false && options[:partition_key] && options[:partition_key].to_s != 'id'
+    execute "ALTER TABLE #{table_name} DROP CONSTRAINT #{table_name}_pkey"
+    execute "ALTER TABLE #{table_name} ADD PRIMARY KEY(\"#{options[:partition_key]}\", id)"
+  end
+  ret
+end
+
+
+ +
+

+ + #orig_create_tableObject + + + + + +

+ + + + +
+
+
+
+72
+
+
# File 'lib/activerecord-multi-tenant/migrations.rb', line 72
+
+alias orig_create_table create_table
+
+
+ +
+ +
+ + + +
+ + \ No newline at end of file diff --git a/docs/source/_static/api-reference/ActiveRecord/QueryMethods.html b/docs/source/_static/api-reference/ActiveRecord/QueryMethods.html new file mode 100644 index 00000000..f0ad0103 --- /dev/null +++ b/docs/source/_static/api-reference/ActiveRecord/QueryMethods.html @@ -0,0 +1,336 @@ + + + + + + + Module: ActiveRecord::QueryMethods + + — Documentation by YARD 0.9.34 + + + + + + + + + + + + + + + + + + + +
+ + +

Module: ActiveRecord::QueryMethods + + + +

+
+ + + + + + + + + + + +
+
Defined in:
+
lib/activerecord-multi-tenant/query_rewriter.rb
+
+ +
+ + + + + + + + + +

+ Instance Method Summary + collapse +

+ + + + + + +
+

Instance Method Details

+ + +
+

+ + #build_arel(*args) ⇒ Object + + + + + +

+ + + + +
+
+
+
+288
+289
+290
+291
+292
+293
+294
+295
+296
+297
+298
+299
+300
+301
+302
+303
+304
+305
+306
+307
+308
+309
+310
+311
+312
+313
+314
+315
+316
+317
+318
+319
+320
+321
+322
+323
+324
+325
+326
+327
+328
+329
+330
+331
+332
+333
+334
+335
+336
+337
+338
+339
+340
+341
+342
+343
+344
+345
+346
+347
+
+
# File 'lib/activerecord-multi-tenant/query_rewriter.rb', line 288
+
+def build_arel(*args)
+  arel = build_arel_orig(*args)
+
+  unless MultiTenant.with_write_only_mode_enabled?
+    visitor = MultiTenant::ArelTenantVisitor.new(arel)
+
+    visitor.contexts.each do |context|
+      node = context.arel_node
+
+      context.unhandled_relations.each do |relation|
+        model = MultiTenant.multi_tenant_model_for_table(relation.arel_table.table_name)
+
+        if MultiTenant.current_tenant_id
+          enforcement_clause = MultiTenant::TenantEnforcementClause.new(relation.arel_table[model.partition_key])
+          case node
+          when Arel::Nodes::Join # Arel::Nodes::OuterJoin, Arel::Nodes::RightOuterJoin, Arel::Nodes::FullOuterJoin
+            node.right.expr = node.right.expr.and(enforcement_clause)
+          when Arel::Nodes::SelectCore
+            if node.wheres.empty?
+              node.wheres = [enforcement_clause]
+            elsif node.wheres[0].is_a?(Arel::Nodes::And)
+              node.wheres[0].children << enforcement_clause
+            else
+              node.wheres[0] = enforcement_clause.and(node.wheres[0])
+            end
+          else
+            raise 'UnknownContext'
+          end
+        end
+
+        next unless node.is_a?(Arel::Nodes::SelectCore) || node.is_a?(Arel::Nodes::Join)
+
+        node_list = if node.is_a? Arel::Nodes::Join
+                      [node]
+                    else
+                      node.source.right
+                    end
+
+        node_list.select { |n| n.is_a? Arel::Nodes::Join }.each do |node_join|
+          next unless node_join.right
+
+          relation_right, relation_left = relations_from_node_join(node_join)
+
+          next unless relation_right && relation_left
+
+          model_right = MultiTenant.multi_tenant_model_for_table(relation_left.table_name)
+          model_left = MultiTenant.multi_tenant_model_for_table(relation_right.table_name)
+          next unless model_right && model_left
+
+          join_enforcement_clause = MultiTenant::TenantJoinEnforcementClause.new(
+            relation_right[model_right.partition_key], relation_left
+          )
+          node_join.right.expr = node_join.right.expr.and(join_enforcement_clause)
+        end
+      end
+    end
+  end
+
+  arel
+end
+
+
+ +
+

+ + #build_arel_origObject + + + + + +

+ + + + +
+
+
+
+286
+
+
# File 'lib/activerecord-multi-tenant/query_rewriter.rb', line 286
+
+alias build_arel_orig build_arel
+
+
+ +
+ +
+ + + +
+ + \ No newline at end of file diff --git a/docs/source/_static/api-reference/ActiveRecord/SchemaDumper.html b/docs/source/_static/api-reference/ActiveRecord/SchemaDumper.html new file mode 100644 index 00000000..71845cbc --- /dev/null +++ b/docs/source/_static/api-reference/ActiveRecord/SchemaDumper.html @@ -0,0 +1,121 @@ + + + + + + + Class: ActiveRecord::SchemaDumper + + — Documentation by YARD 0.9.34 + + + + + + + + + + + + + + + + + + + +
+ + +

Class: ActiveRecord::SchemaDumper + + + +

+
+ +
+
Inherits:
+
+ Object + +
    +
  • Object
  • + + + +
+ show all + +
+
+ + + + + + + + + + + +
+
Defined in:
+
lib/activerecord-multi-tenant/migrations.rb
+
+ +
+ + + + + + + + + + +
+ + + +
+ + \ No newline at end of file diff --git a/docs/source/_static/api-reference/MultiTenant.html b/docs/source/_static/api-reference/MultiTenant.html new file mode 100644 index 00000000..91cf13a5 --- /dev/null +++ b/docs/source/_static/api-reference/MultiTenant.html @@ -0,0 +1,1454 @@ + + + + + + + Module: MultiTenant + + — Documentation by YARD 0.9.34 + + + + + + + + + + + + + + + + + + + +
+ + +

Module: MultiTenant + + + +

+
+ + + + + + + + + + + +
+
Defined in:
+
lib/activerecord-multi-tenant/fast_truncate.rb,
+ lib/activerecord-multi-tenant/version.rb,
lib/activerecord-multi-tenant/migrations.rb,
lib/activerecord-multi-tenant/multi_tenant.rb,
lib/activerecord-multi-tenant/query_monitor.rb,
lib/activerecord-multi-tenant/query_rewriter.rb,
lib/activerecord-multi-tenant/copy_from_client.rb,
lib/activerecord-multi-tenant/model_extensions.rb,
lib/activerecord-multi-tenant/controller_extensions.rb,
lib/activerecord-multi-tenant/arel_visitors_depth_first.rb
+
+
+ +
+ +

Overview

+
+ +

Extension to the controller to allow setting the current tenant set_current_tenant and current_tenant methods are introduced to set and get the current tenant in the controllers that uses the MultiTenant module.

+ + +
+
+
+ + +

Defined Under Namespace

+

+ + + Modules: ControllerExtensions, CopyFromClient, DatabaseStatements, FastTruncate, MigrationExtensions, ModelExtensionsClassMethods, TenantValueVisitor + + + + Classes: ArelTenantVisitor, ArelVisitorsDepthFirst, BaseTenantEnforcementClause, Context, CopyFromClientHelper, Current, MissingTenantError, QueryMonitor, Table, TenantEnforcementClause, TenantIsImmutable, TenantJoinEnforcementClause + + +

+ + +

+ Constant Summary + collapse +

+ +
+ +
VERSION = + +
+
'2.2.0'.freeze
+ +
@@enable_query_monitor = +
+
+ +

rubocop:disable Style/ClassVars Option to enable query monitor

+ + +
+
+
+ + +
+
+
false
+ +
+ + + + + + + + + +

+ Class Method Summary + collapse +

+ + + + + + +
+

Class Method Details

+ + +
+

+ + .current_tenantObject + + + + + +

+ + + + +
+
+
+
+71
+72
+73
+
+
# File 'lib/activerecord-multi-tenant/multi_tenant.rb', line 71
+
+def self.current_tenant
+  Current.tenant
+end
+
+
+ +
+

+ + .current_tenant=(tenant) ⇒ Object + + + + + +

+ + + + +
+
+
+
+67
+68
+69
+
+
# File 'lib/activerecord-multi-tenant/multi_tenant.rb', line 67
+
+def self.current_tenant=(tenant)
+  Current.tenant = tenant
+end
+
+
+ +
+

+ + .current_tenant_classObject + + + + + +

+ + + + +
+
+
+
+83
+84
+85
+86
+87
+88
+89
+
+
# File 'lib/activerecord-multi-tenant/multi_tenant.rb', line 83
+
+def self.current_tenant_class
+  if current_tenant_is_id?
+    MultiTenant.default_tenant_class || raise('Only have tenant id, and no default tenant class set')
+  elsif current_tenant
+    MultiTenant.current_tenant.class.name
+  end
+end
+
+
+ +
+

+ + .current_tenant_idObject + + + + + +

+ + + + +
+
+
+
+75
+76
+77
+
+
# File 'lib/activerecord-multi-tenant/multi_tenant.rb', line 75
+
+def self.current_tenant_id
+  current_tenant_is_id? ? current_tenant : current_tenant.try(:id)
+end
+
+
+ +
+

+ + .current_tenant_is_id?Boolean + + + + + +

+
+ + +
+
+
+ +

Returns:

+
    + +
  • + + + (Boolean) + + + +
  • + +
+ +
+ + + + +
+
+
+
+79
+80
+81
+
+
# File 'lib/activerecord-multi-tenant/multi_tenant.rb', line 79
+
+def self.current_tenant_is_id?
+  current_tenant.is_a?(String) || current_tenant.is_a?(Integer)
+end
+
+
+ +
+

+ + .default_tenant_classObject + + + + + +

+ + + + +
+
+
+
+22
+23
+24
+
+
# File 'lib/activerecord-multi-tenant/multi_tenant.rb', line 22
+
+def self.default_tenant_class
+  @@default_tenant_class ||= nil
+end
+
+
+ +
+

+ + .default_tenant_class=(tenant_class) ⇒ Object + + + + + +

+
+ +

rubocop:disable Style/ClassVars In some cases we only have an ID - if defined we’ll return the default tenant class in such cases

+ + +
+
+
+ + +
+ + + + +
+
+
+
+18
+19
+20
+
+
# File 'lib/activerecord-multi-tenant/multi_tenant.rb', line 18
+
+def self.default_tenant_class=(tenant_class)
+  @@default_tenant_class = tenant_class
+end
+
+
+ +
+

+ + .enable_query_monitorObject + + + + + +

+ + + + +
+
+
+
+10
+11
+12
+
+
# File 'lib/activerecord-multi-tenant/query_monitor.rb', line 10
+
+def self.enable_query_monitor
+  @@enable_query_monitor = true
+end
+
+
+ +
+

+ + .enable_write_only_modeObject + + + + + +

+
+ +

Write-only Mode - this only adds the tenant_id to new records, but doesn’t require its presence for SELECTs/UPDATEs/DELETEs

+ + +
+
+
+ + +
+ + + + +
+
+
+
+28
+29
+30
+
+
# File 'lib/activerecord-multi-tenant/multi_tenant.rb', line 28
+
+def self.enable_write_only_mode
+  @@enable_write_only_mode = true
+end
+
+
+ +
+

+ + .load_current_tenant!Object + + + + + +

+ + + + +
+
+
+
+91
+92
+93
+94
+95
+96
+97
+
+
# File 'lib/activerecord-multi-tenant/multi_tenant.rb', line 91
+
+def self.load_current_tenant!
+  return MultiTenant.current_tenant if MultiTenant.current_tenant && !current_tenant_is_id?
+  raise 'MultiTenant.current_tenant must be set to load' if MultiTenant.current_tenant.nil?
+
+  klass = MultiTenant.default_tenant_class || raise('Only have tenant id, and no default tenant class set')
+  self.current_tenant = klass.find(MultiTenant.current_tenant_id)
+end
+
+
+ +
+

+ + .multi_tenant_model_for_arel(arel) ⇒ Object + + + + + +

+ + + + +
+
+
+
+57
+58
+59
+60
+61
+62
+63
+64
+65
+
+
# File 'lib/activerecord-multi-tenant/multi_tenant.rb', line 57
+
+def self.multi_tenant_model_for_arel(arel)
+  return nil unless arel.respond_to?(:ast)
+
+  if arel.ast.relation.is_a? Arel::Nodes::JoinSource
+    MultiTenant.multi_tenant_model_for_table(arel.ast.relation.left.table_name)
+  else
+    MultiTenant.multi_tenant_model_for_table(arel.ast.relation.table_name)
+  end
+end
+
+
+ +
+

+ + .multi_tenant_model_for_table(table_name) ⇒ Object + + + + + +

+ + + + +
+
+
+
+44
+45
+46
+47
+48
+49
+50
+51
+52
+53
+54
+55
+
+
# File 'lib/activerecord-multi-tenant/multi_tenant.rb', line 44
+
+def self.multi_tenant_model_for_table(table_name)
+  @@multi_tenant_models ||= []
+
+  unless defined?(@@multi_tenant_model_table_names)
+    @@multi_tenant_model_table_names = @@multi_tenant_models.map do |model|
+      [model.table_name, model] if model.table_name
+    end.compact.to_h
+  end
+
+  @@multi_tenant_model_table_names[table_name.to_s]
+  # rubocop:enable Style/ClassVars
+end
+
+
+ +
+

+ + .partition_key(tenant_name) ⇒ Object + + + + + +

+ + + + +
+
+
+
+12
+13
+14
+
+
# File 'lib/activerecord-multi-tenant/multi_tenant.rb', line 12
+
+def self.partition_key(tenant_name)
+  "#{tenant_name}_id"
+end
+
+
+ +
+

+ + .query_monitor_enabled?Boolean + + + + + +

+
+ + +
+
+
+ +

Returns:

+
    + +
  • + + + (Boolean) + + + +
  • + +
+ +
+ + + + +
+
+
+
+14
+15
+16
+
+
# File 'lib/activerecord-multi-tenant/query_monitor.rb', line 14
+
+def self.query_monitor_enabled?
+  @@enable_query_monitor
+end
+
+
+ +
+

+ + .register_multi_tenant_model(model_klass) ⇒ Object + + + + + +

+
+ +

Registry that maps table names to models (used by the query rewriter)

+ + +
+
+
+ + +
+ + + + +
+
+
+
+37
+38
+39
+40
+41
+42
+
+
# File 'lib/activerecord-multi-tenant/multi_tenant.rb', line 37
+
+def self.register_multi_tenant_model(model_klass)
+  @@multi_tenant_models ||= []
+  @@multi_tenant_models.push(model_klass)
+
+  remove_class_variable(:@@multi_tenant_model_table_names) if defined?(@@multi_tenant_model_table_names)
+end
+
+
+ +
+

+ + .tenant_klass_defined?(tenant_name) ⇒ Boolean + + + + + +

+
+ + +
+
+
+ +

Returns:

+
    + +
  • + + + (Boolean) + + + +
  • + +
+ +
+ + + + +
+
+
+
+8
+9
+10
+
+
# File 'lib/activerecord-multi-tenant/multi_tenant.rb', line 8
+
+def self.tenant_klass_defined?(tenant_name)
+  !!tenant_name.to_s.classify.safe_constantize
+end
+
+
+ +
+

+ + .with(tenant, &block) ⇒ Object + + + + + +

+ + + + +
+
+
+
+99
+100
+101
+102
+103
+104
+105
+106
+107
+108
+109
+
+
# File 'lib/activerecord-multi-tenant/multi_tenant.rb', line 99
+
+def self.with(tenant, &block)
+  return block.call if current_tenant == tenant
+
+  old_tenant = current_tenant
+  begin
+    self.current_tenant = tenant
+    block.call
+  ensure
+    self.current_tenant = old_tenant
+  end
+end
+
+
+ +
+

+ + .with_write_only_mode_enabled?Boolean + + + + + +

+
+ + +
+
+
+ +

Returns:

+
    + +
  • + + + (Boolean) + + + +
  • + +
+ +
+ + + + +
+
+
+
+32
+33
+34
+
+
# File 'lib/activerecord-multi-tenant/multi_tenant.rb', line 32
+
+def self.with_write_only_mode_enabled?
+  @@enable_write_only_mode ||= false
+end
+
+
+ +
+

+ + .without(&block) ⇒ Object + + + + + +

+ + + + +
+
+
+
+111
+112
+113
+114
+115
+116
+117
+118
+119
+120
+121
+
+
# File 'lib/activerecord-multi-tenant/multi_tenant.rb', line 111
+
+def self.without(&block)
+  return block.call if current_tenant.nil?
+
+  old_tenant = current_tenant
+  begin
+    self.current_tenant = nil
+    block.call
+  ensure
+    self.current_tenant = old_tenant
+  end
+end
+
+
+ +
+

+ + .wrap_methods(klass, owner, *method_names) ⇒ Object + + + + + +

+ + + + +
+
+
+
+129
+130
+131
+132
+133
+134
+135
+136
+137
+138
+139
+140
+141
+142
+143
+
+
# File 'lib/activerecord-multi-tenant/multi_tenant.rb', line 129
+
+def self.wrap_methods(klass, owner, *method_names)
+  method_names.each do |method_name|
+    original_method_name = :"_mt_original_#{method_name}"
+    klass.class_eval <<-CODE, __FILE__, __LINE__ + 1
+      alias_method :#{original_method_name}, :#{method_name}
+      def #{method_name}(*args, &block)
+        if MultiTenant.multi_tenant_model_for_table(#{owner}.class.table_name).present? && #{owner}.persisted? && MultiTenant.current_tenant_id.nil? && #{owner}.class.respond_to?(:partition_key) && #{owner}.attributes.include?(#{owner}.class.partition_key)
+          MultiTenant.with(#{owner}.public_send(#{owner}.class.partition_key)) { #{original_method_name}(*args, &block) }
+        else
+          #{original_method_name}(*args, &block)
+        end
+      end
+    CODE
+  end
+end
+
+
+ +
+ +
+ + + +
+ + \ No newline at end of file diff --git a/docs/source/_static/api-reference/MultiTenant/ArelTenantVisitor.html b/docs/source/_static/api-reference/MultiTenant/ArelTenantVisitor.html new file mode 100644 index 00000000..c27cf8b9 --- /dev/null +++ b/docs/source/_static/api-reference/MultiTenant/ArelTenantVisitor.html @@ -0,0 +1,755 @@ + + + + + + + Class: MultiTenant::ArelTenantVisitor + + — Documentation by YARD 0.9.34 + + + + + + + + + + + + + + + + + + + +
+ + +

Class: MultiTenant::ArelTenantVisitor + + + +

+
+ +
+
Inherits:
+
+ Object + +
    +
  • Object
  • + + + +
+ show all + +
+
+ + + + + + + + + + + +
+
Defined in:
+
lib/activerecord-multi-tenant/query_rewriter.rb
+
+ +
+ + + + + +

Instance Attribute Summary collapse

+ + + + + + +

+ Instance Method Summary + collapse +

+ + + + +
+

Constructor Details

+ +
+

+ + #initialize(arel) ⇒ ArelTenantVisitor + + + + + +

+
+ +

Returns a new instance of ArelTenantVisitor.

+ + +
+
+
+ + +
+ + + + +
+
+
+
+66
+67
+68
+69
+70
+71
+72
+73
+
+
# File 'lib/activerecord-multi-tenant/query_rewriter.rb', line 66
+
+def initialize(arel)
+  super(proc {})
+  @statement_node_id = nil
+
+  @contexts = []
+  @current_context = nil
+  accept(arel.ast)
+end
+
+
+ +
+ +
+

Instance Attribute Details

+ + + +
+

+ + #contextsObject (readonly) + + + + + +

+
+ +

Returns the value of attribute contexts.

+ + +
+
+
+ + +
+ + + + +
+
+
+
+75
+76
+77
+
+
# File 'lib/activerecord-multi-tenant/query_rewriter.rb', line 75
+
+def contexts
+  @contexts
+end
+
+
+ +
+ + +
+

Instance Method Details

+ + +
+

+ + #visit_Arel_Attributes_Attribute(*args) ⇒ Object + + + + + +

+
+ +

rubocop:disable Naming/MethodName

+ + +
+
+
+ + +
+ + + + +
+
+
+
+78
+79
+80
+81
+82
+
+
# File 'lib/activerecord-multi-tenant/query_rewriter.rb', line 78
+
+def visit_Arel_Attributes_Attribute(*args)
+  return if @current_context.nil?
+
+  super(*args)
+end
+
+
+ +
+

+ + #visit_Arel_Nodes_Equality(obj, *args) ⇒ Object + + + + + +

+ + + + +
+
+
+
+84
+85
+86
+87
+88
+89
+90
+91
+92
+93
+
+
# File 'lib/activerecord-multi-tenant/query_rewriter.rb', line 84
+
+def visit_Arel_Nodes_Equality(obj, *args)
+  if obj.left.is_a?(Arel::Attributes::Attribute)
+    table_name = obj.left.relation.table_name
+    model = MultiTenant.multi_tenant_model_for_table(table_name)
+    if model.present? && obj.left.name.to_s == model.partition_key.to_s
+      @current_context.visited_handled_relation(obj.left.relation)
+    end
+  end
+  super(obj, *args)
+end
+
+
+ +
+

+ + #visit_Arel_Nodes_OuterJoin(obj, _collector = nil) ⇒ Object + + + + Also known as: + visit_Arel_Nodes_FullOuterJoin, visit_Arel_Nodes_RightOuterJoin + + + + +

+
+ +

rubocop:disable Naming/MethodName

+ + +
+
+
+ + +
+ + + + +
+
+
+
+128
+129
+130
+131
+132
+133
+134
+135
+
+
# File 'lib/activerecord-multi-tenant/query_rewriter.rb', line 128
+
+def visit_Arel_Nodes_OuterJoin(obj, _collector = nil)
+  nest_context(obj) do
+    @current_context.discover_relations do
+      visit obj.left
+      visit obj.right
+    end
+  end
+end
+
+
+ +
+

+ + #visit_Arel_Nodes_SelectCore(obj, *_args) ⇒ Object + + + + + +

+ + + + +
+
+
+
+109
+110
+111
+112
+113
+114
+115
+116
+117
+118
+119
+120
+121
+122
+123
+
+
# File 'lib/activerecord-multi-tenant/query_rewriter.rb', line 109
+
+def visit_Arel_Nodes_SelectCore(obj, *_args)
+  nest_context(obj) do
+    @current_context.discover_relations do
+      visit obj.source
+    end
+    visit obj.wheres
+    visit obj.groups
+    visit obj.windows
+    if defined?(obj.having)
+      visit obj.having
+    else
+      visit obj.havings
+    end
+  end
+end
+
+
+ +
+

+ + #visit_Arel_Table(obj, _collector = nil) ⇒ Object + + + + Also known as: + visit_Arel_Nodes_TableAlias + + + + +

+ + + + +
+
+
+
+103
+104
+105
+
+
# File 'lib/activerecord-multi-tenant/query_rewriter.rb', line 103
+
+def visit_Arel_Table(obj, _collector = nil)
+  @current_context.visited_relation(obj) if tenant_relation?(obj.table_name)
+end
+
+
+ +
+

+ + #visit_MultiTenant_TenantEnforcementClause(obj) ⇒ Object + + + + + +

+ + + + +
+
+
+
+95
+96
+97
+
+
# File 'lib/activerecord-multi-tenant/query_rewriter.rb', line 95
+
+def visit_MultiTenant_TenantEnforcementClause(obj, *)
+  @current_context.visited_handled_relation(obj.tenant_attribute.relation)
+end
+
+
+ +
+

+ + #visit_MultiTenant_TenantJoinEnforcementClause(obj) ⇒ Object + + + + + +

+ + + + +
+
+
+
+99
+100
+101
+
+
# File 'lib/activerecord-multi-tenant/query_rewriter.rb', line 99
+
+def visit_MultiTenant_TenantJoinEnforcementClause(obj, *)
+  @current_context.visited_handled_relation(obj.tenant_attribute.relation)
+end
+
+
+ +
+ +
+ + + +
+ + \ No newline at end of file diff --git a/docs/source/_static/api-reference/MultiTenant/ArelVisitorsDepthFirst.html b/docs/source/_static/api-reference/MultiTenant/ArelVisitorsDepthFirst.html new file mode 100644 index 00000000..2bb4c0f6 --- /dev/null +++ b/docs/source/_static/api-reference/MultiTenant/ArelVisitorsDepthFirst.html @@ -0,0 +1,208 @@ + + + + + + + Class: MultiTenant::ArelVisitorsDepthFirst + + — Documentation by YARD 0.9.34 + + + + + + + + + + + + + + + + + + + +
+ + +

Class: MultiTenant::ArelVisitorsDepthFirst + + + +

+
+ +
+
Inherits:
+
+ Arel::Visitors::Visitor + +
    +
  • Object
  • + + + + + +
+ show all + +
+
+ + + + + + + + + + + +
+
Defined in:
+
lib/activerecord-multi-tenant/arel_visitors_depth_first.rb
+
+ +
+ + + + + + + + + +

+ Instance Method Summary + collapse +

+ + + + + +
+

Constructor Details

+ +
+

+ + #initialize(block = nil) ⇒ ArelVisitorsDepthFirst + + + + + +

+
+ +

Returns a new instance of ArelVisitorsDepthFirst.

+ + +
+
+
+ + +
+ + + + +
+
+
+
+5
+6
+7
+8
+
+
# File 'lib/activerecord-multi-tenant/arel_visitors_depth_first.rb', line 5
+
+def initialize(block = nil)
+  @block = block || proc
+  super()
+end
+
+
+ +
+ + +
+ + + +
+ + \ No newline at end of file diff --git a/docs/source/_static/api-reference/MultiTenant/BaseTenantEnforcementClause.html b/docs/source/_static/api-reference/MultiTenant/BaseTenantEnforcementClause.html new file mode 100644 index 00000000..e8f67a0a --- /dev/null +++ b/docs/source/_static/api-reference/MultiTenant/BaseTenantEnforcementClause.html @@ -0,0 +1,462 @@ + + + + + + + Class: MultiTenant::BaseTenantEnforcementClause + + — Documentation by YARD 0.9.34 + + + + + + + + + + + + + + + + + + + +
+ + +

Class: MultiTenant::BaseTenantEnforcementClause + + + +

+
+ +
+
Inherits:
+
+ Arel::Nodes::Node + +
    +
  • Object
  • + + + + + +
+ show all + +
+
+ + + + + + + + + + + +
+
Defined in:
+
lib/activerecord-multi-tenant/query_rewriter.rb
+
+ +
+ + + + + + +

Instance Attribute Summary collapse

+ + + + + + +

+ Instance Method Summary + collapse +

+ + + + + +
+

Constructor Details

+ +
+

+ + #initialize(tenant_attribute) ⇒ BaseTenantEnforcementClause + + + + + +

+
+ +

Returns a new instance of BaseTenantEnforcementClause.

+ + +
+
+
+ + +
+ + + + +
+
+
+
+179
+180
+181
+182
+183
+
+
# File 'lib/activerecord-multi-tenant/query_rewriter.rb', line 179
+
+def initialize(tenant_attribute)
+  super()
+  @tenant_attribute = tenant_attribute
+  @tenant_model = MultiTenant.multi_tenant_model_for_table(tenant_attribute.relation.table_name)
+end
+
+
+ +
+ +
+

Instance Attribute Details

+ + + +
+

+ + #tenant_attributeObject (readonly) + + + + + +

+
+ +

Returns the value of attribute tenant_attribute.

+ + +
+
+
+ + +
+ + + + +
+
+
+
+177
+178
+179
+
+
# File 'lib/activerecord-multi-tenant/query_rewriter.rb', line 177
+
+def tenant_attribute
+  @tenant_attribute
+end
+
+
+ +
+ + +
+

Instance Method Details

+ + +
+

+ + #to_sObject + + + + + +

+ + + + +
+
+
+
+185
+186
+187
+
+
# File 'lib/activerecord-multi-tenant/query_rewriter.rb', line 185
+
+def to_s
+  to_sql
+end
+
+
+ +
+

+ + #to_sqlObject + + + + + +

+ + + + +
+
+
+
+193
+194
+195
+196
+197
+
+
# File 'lib/activerecord-multi-tenant/query_rewriter.rb', line 193
+
+def to_sql(*)
+  collector = Arel::Collectors::SQLString.new
+  collector = @tenant_model.connection.visitor.accept tenant_arel, collector
+  collector.value
+end
+
+
+ +
+

+ + #to_strObject + + + + + +

+ + + + +
+
+
+
+189
+190
+191
+
+
# File 'lib/activerecord-multi-tenant/query_rewriter.rb', line 189
+
+def to_str
+  to_sql
+end
+
+
+ +
+ +
+ + + +
+ + \ No newline at end of file diff --git a/docs/source/_static/api-reference/MultiTenant/Context.html b/docs/source/_static/api-reference/MultiTenant/Context.html new file mode 100644 index 00000000..2d1fa6dc --- /dev/null +++ b/docs/source/_static/api-reference/MultiTenant/Context.html @@ -0,0 +1,659 @@ + + + + + + + Class: MultiTenant::Context + + — Documentation by YARD 0.9.34 + + + + + + + + + + + + + + + + + + + +
+ + +

Class: MultiTenant::Context + + + +

+
+ +
+
Inherits:
+
+ Object + +
    +
  • Object
  • + + + +
+ show all + +
+
+ + + + + + + + + + + +
+
Defined in:
+
lib/activerecord-multi-tenant/query_rewriter.rb
+
+ +
+ + + + + +

Instance Attribute Summary collapse

+ + + + + + +

+ Instance Method Summary + collapse +

+ + + + +
+

Constructor Details

+ +
+

+ + #initialize(arel_node) ⇒ Context + + + + + +

+
+ +

Returns a new instance of Context.

+ + +
+
+
+ + +
+ + + + +
+
+
+
+32
+33
+34
+35
+36
+37
+
+
# File 'lib/activerecord-multi-tenant/query_rewriter.rb', line 32
+
+def initialize(arel_node)
+  @arel_node = arel_node
+  @known_relations = []
+  @handled_relations = []
+  @discovering = false
+end
+
+
+ +
+ +
+

Instance Attribute Details

+ + + +
+

+ + #arel_nodeObject (readonly) + + + + + +

+
+ +

Returns the value of attribute arel_node.

+ + +
+
+
+ + +
+ + + + +
+
+
+
+30
+31
+32
+
+
# File 'lib/activerecord-multi-tenant/query_rewriter.rb', line 30
+
+def arel_node
+  @arel_node
+end
+
+
+ + + +
+

+ + #handled_relationsObject (readonly) + + + + + +

+
+ +

Returns the value of attribute handled_relations.

+ + +
+
+
+ + +
+ + + + +
+
+
+
+30
+31
+32
+
+
# File 'lib/activerecord-multi-tenant/query_rewriter.rb', line 30
+
+def handled_relations
+  @handled_relations
+end
+
+
+ + + +
+

+ + #known_relationsObject (readonly) + + + + + +

+
+ +

Returns the value of attribute known_relations.

+ + +
+
+
+ + +
+ + + + +
+
+
+
+30
+31
+32
+
+
# File 'lib/activerecord-multi-tenant/query_rewriter.rb', line 30
+
+def known_relations
+  @known_relations
+end
+
+
+ +
+ + +
+

Instance Method Details

+ + +
+

+ + #discover_relationsObject + + + + + +

+ + + + +
+
+
+
+39
+40
+41
+42
+43
+44
+
+
# File 'lib/activerecord-multi-tenant/query_rewriter.rb', line 39
+
+def discover_relations
+  old_discovering = @discovering
+  @discovering = true
+  yield
+  @discovering = old_discovering
+end
+
+
+ +
+

+ + #unhandled_relationsObject + + + + + +

+ + + + +
+
+
+
+56
+57
+58
+
+
# File 'lib/activerecord-multi-tenant/query_rewriter.rb', line 56
+
+def unhandled_relations
+  known_relations.uniq - handled_relations
+end
+
+
+ +
+

+ + #visited_handled_relation(relation) ⇒ Object + + + + + +

+ + + + +
+
+
+
+52
+53
+54
+
+
# File 'lib/activerecord-multi-tenant/query_rewriter.rb', line 52
+
+def visited_handled_relation(relation)
+  @handled_relations << Table.new(relation)
+end
+
+
+ +
+

+ + #visited_relation(relation) ⇒ Object + + + + + +

+ + + + +
+
+
+
+46
+47
+48
+49
+50
+
+
# File 'lib/activerecord-multi-tenant/query_rewriter.rb', line 46
+
+def visited_relation(relation)
+  return unless @discovering
+
+  @known_relations << Table.new(relation)
+end
+
+
+ +
+ +
+ + + +
+ + \ No newline at end of file diff --git a/docs/source/_static/api-reference/MultiTenant/ControllerExtensions.html b/docs/source/_static/api-reference/MultiTenant/ControllerExtensions.html new file mode 100644 index 00000000..20b69bc3 --- /dev/null +++ b/docs/source/_static/api-reference/MultiTenant/ControllerExtensions.html @@ -0,0 +1,202 @@ + + + + + + + Module: MultiTenant::ControllerExtensions + + — Documentation by YARD 0.9.34 + + + + + + + + + + + + + + + + + + + +
+ + +

Module: MultiTenant::ControllerExtensions + + + +

+
+ + + + + + + + + + + +
+
Defined in:
+
lib/activerecord-multi-tenant/controller_extensions.rb
+
+ +
+ + + + + + + + + +

+ Instance Method Summary + collapse +

+ + + + + + +
+

Instance Method Details

+ + +
+

+ + #set_current_tenant_through_filterObject + + + + + +

+ + + + +
+
+
+
+9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+
+
# File 'lib/activerecord-multi-tenant/controller_extensions.rb', line 9
+
+def set_current_tenant_through_filter
+  class_eval do
+    helper_method :current_tenant if respond_to?(:helper_method)
+
+    private
+
+    # rubocop:disable Naming/AccessorMethodName
+    def set_current_tenant(current_tenant_object)
+      MultiTenant.current_tenant = current_tenant_object
+    end
+    # rubocop:enable Naming/AccessorMethodName
+
+    def current_tenant
+      MultiTenant.current_tenant
+    end
+  end
+end
+
+
+ +
+ +
+ + + +
+ + \ No newline at end of file diff --git a/docs/source/_static/api-reference/MultiTenant/CopyFromClient.html b/docs/source/_static/api-reference/MultiTenant/CopyFromClient.html new file mode 100644 index 00000000..de6a7790 --- /dev/null +++ b/docs/source/_static/api-reference/MultiTenant/CopyFromClient.html @@ -0,0 +1,186 @@ + + + + + + + Module: MultiTenant::CopyFromClient + + — Documentation by YARD 0.9.34 + + + + + + + + + + + + + + + + + + + +
+ + +

Module: MultiTenant::CopyFromClient + + + +

+
+ + + + + + + + + + + +
+
Defined in:
+
lib/activerecord-multi-tenant/copy_from_client.rb
+
+ +
+ + + + + + + + + +

+ Instance Method Summary + collapse +

+ + + + + + +
+

Instance Method Details

+ + +
+

+ + #copy_from_client(columns, &block) ⇒ Object + + + + + +

+ + + + +
+
+
+
+22
+23
+24
+25
+26
+27
+28
+29
+30
+
+
# File 'lib/activerecord-multi-tenant/copy_from_client.rb', line 22
+
+def copy_from_client(columns, &block)
+  conn         = connection.raw_connection
+  column_types = columns.map { |c| type_for_attribute(c.to_s) }
+  helper = MultiTenant::CopyFromClientHelper.new(conn, column_types)
+  conn.copy_data %{COPY #{quoted_table_name}("#{columns.join('","')}") FROM STDIN}, PG::TextEncoder::CopyRow.new do
+    block.call helper
+  end
+  helper.count
+end
+
+
+ +
+ +
+ + + +
+ + \ No newline at end of file diff --git a/docs/source/_static/api-reference/MultiTenant/CopyFromClientHelper.html b/docs/source/_static/api-reference/MultiTenant/CopyFromClientHelper.html new file mode 100644 index 00000000..25028d7e --- /dev/null +++ b/docs/source/_static/api-reference/MultiTenant/CopyFromClientHelper.html @@ -0,0 +1,362 @@ + + + + + + + Class: MultiTenant::CopyFromClientHelper + + — Documentation by YARD 0.9.34 + + + + + + + + + + + + + + + + + + + +
+ + +

Class: MultiTenant::CopyFromClientHelper + + + +

+
+ +
+
Inherits:
+
+ Object + +
    +
  • Object
  • + + + +
+ show all + +
+
+ + + + + + + + + + + +
+
Defined in:
+
lib/activerecord-multi-tenant/copy_from_client.rb
+
+ +
+ +

Overview

+
+ +

Designed to be mixed into an ActiveRecord model to provide a copy_from_client method that allows for efficient bulk insertion of data into a PostgreSQL database using the COPY command

+ + +
+
+
+ + +
+ + + +

Instance Attribute Summary collapse

+ + + + + + +

+ Instance Method Summary + collapse +

+ + + + +
+

Constructor Details

+ +
+

+ + #initialize(conn, column_types) ⇒ CopyFromClientHelper + + + + + +

+
+ +

Returns a new instance of CopyFromClientHelper.

+ + +
+
+
+ + +
+ + + + +
+
+
+
+8
+9
+10
+11
+12
+
+
# File 'lib/activerecord-multi-tenant/copy_from_client.rb', line 8
+
+def initialize(conn, column_types)
+  @count = 0
+  @conn = conn
+  @column_types = column_types
+end
+
+
+ +
+ +
+

Instance Attribute Details

+ + + +
+

+ + #countObject (readonly) + + + + + +

+
+ +

Returns the value of attribute count.

+ + +
+
+
+ + +
+ + + + +
+
+
+
+6
+7
+8
+
+
# File 'lib/activerecord-multi-tenant/copy_from_client.rb', line 6
+
+def count
+  @count
+end
+
+
+ +
+ + +
+

Instance Method Details

+ + +
+

+ + #<<(row) ⇒ Object + + + + + +

+ + + + +
+
+
+
+14
+15
+16
+17
+18
+
+
# File 'lib/activerecord-multi-tenant/copy_from_client.rb', line 14
+
+def <<(row)
+  row = row.map.with_index { |val, idx| @column_types[idx].serialize(val) }
+  @conn.put_copy_data(row)
+  @count += 1
+end
+
+
+ +
+ +
+ + + +
+ + \ No newline at end of file diff --git a/docs/source/_static/api-reference/MultiTenant/Current.html b/docs/source/_static/api-reference/MultiTenant/Current.html new file mode 100644 index 00000000..c88b2fed --- /dev/null +++ b/docs/source/_static/api-reference/MultiTenant/Current.html @@ -0,0 +1,124 @@ + + + + + + + Class: MultiTenant::Current + + — Documentation by YARD 0.9.34 + + + + + + + + + + + + + + + + + + + +
+ + +

Class: MultiTenant::Current + + + +

+
+ +
+
Inherits:
+
+ ActiveSupport::CurrentAttributes + +
    +
  • Object
  • + + + + + +
+ show all + +
+
+ + + + + + + + + + + +
+
Defined in:
+
lib/activerecord-multi-tenant/multi_tenant.rb
+
+ +
+ + + + + + + + + + + +
+ + + +
+ + \ No newline at end of file diff --git a/docs/source/_static/api-reference/MultiTenant/DatabaseStatements.html b/docs/source/_static/api-reference/MultiTenant/DatabaseStatements.html new file mode 100644 index 00000000..4cedf31d --- /dev/null +++ b/docs/source/_static/api-reference/MultiTenant/DatabaseStatements.html @@ -0,0 +1,366 @@ + + + + + + + Module: MultiTenant::DatabaseStatements + + — Documentation by YARD 0.9.34 + + + + + + + + + + + + + + + + + + + +
+ + +

Module: MultiTenant::DatabaseStatements + + + +

+
+ + + + + + + + + + + +
+
Defined in:
+
lib/activerecord-multi-tenant/query_rewriter.rb
+
+ +
+ + + + + + + + + +

+ Instance Method Summary + collapse +

+ + + + + + +
+

Instance Method Details

+ + +
+

+ + #delete(arel, name = nil, binds = []) ⇒ Object + + + + + +

+ + + + +
+
+
+
+268
+269
+270
+271
+272
+273
+274
+
+
# File 'lib/activerecord-multi-tenant/query_rewriter.rb', line 268
+
+def delete(arel, name = nil, binds = [])
+  model = MultiTenant.multi_tenant_model_for_arel(arel)
+  if model.present? && !MultiTenant.with_write_only_mode_enabled? && MultiTenant.current_tenant_id.present?
+    arel.where(MultiTenant::TenantEnforcementClause.new(model.arel_table[model.partition_key]))
+  end
+  super(arel, name, binds)
+end
+
+
+ +
+

+ + #join_to_delete(delete, *args) ⇒ Object + + + + + +

+ + + + +
+
+
+
+251
+252
+253
+254
+255
+256
+257
+258
+
+
# File 'lib/activerecord-multi-tenant/query_rewriter.rb', line 251
+
+def join_to_delete(delete, *args)
+  delete = super(delete, *args)
+  model = MultiTenant.multi_tenant_model_for_table(delete.ast.left.table_name)
+  if model.present? && !MultiTenant.with_write_only_mode_enabled? && MultiTenant.current_tenant_id.present?
+    delete.where(MultiTenant::TenantEnforcementClause.new(model.arel_table[model.partition_key]))
+  end
+  delete
+end
+
+
+ +
+

+ + #join_to_update(update, *args) ⇒ Object + + + + + +

+ + + + +
+
+
+
+242
+243
+244
+245
+246
+247
+248
+249
+
+
# File 'lib/activerecord-multi-tenant/query_rewriter.rb', line 242
+
+def join_to_update(update, *args)
+  update = super(update, *args)
+  model = MultiTenant.multi_tenant_model_for_table(update.ast.relation.table_name)
+  if model.present? && !MultiTenant.with_write_only_mode_enabled? && MultiTenant.current_tenant_id.present?
+    update.where(MultiTenant::TenantEnforcementClause.new(model.arel_table[model.partition_key]))
+  end
+  update
+end
+
+
+ +
+

+ + #update(arel, name = nil, binds = []) ⇒ Object + + + + + +

+ + + + +
+
+
+
+260
+261
+262
+263
+264
+265
+266
+
+
# File 'lib/activerecord-multi-tenant/query_rewriter.rb', line 260
+
+def update(arel, name = nil, binds = [])
+  model = MultiTenant.multi_tenant_model_for_arel(arel)
+  if model.present? && !MultiTenant.with_write_only_mode_enabled? && MultiTenant.current_tenant_id.present?
+    arel.where(MultiTenant::TenantEnforcementClause.new(model.arel_table[model.partition_key]))
+  end
+  super(arel, name, binds)
+end
+
+
+ +
+ +
+ + + +
+ + \ No newline at end of file diff --git a/docs/source/_static/api-reference/MultiTenant/FastTruncate.html b/docs/source/_static/api-reference/MultiTenant/FastTruncate.html new file mode 100644 index 00000000..d799e294 --- /dev/null +++ b/docs/source/_static/api-reference/MultiTenant/FastTruncate.html @@ -0,0 +1,226 @@ + + + + + + + Module: MultiTenant::FastTruncate + + — Documentation by YARD 0.9.34 + + + + + + + + + + + + + + + + + + + +
+ + +

Module: MultiTenant::FastTruncate + + + +

+
+ + + + + + + + + + + +
+
Defined in:
+
lib/activerecord-multi-tenant/fast_truncate.rb
+
+ +
+ + + + + + + + + +

+ Class Method Summary + collapse +

+ + + + + + +
+

Class Method Details

+ + +
+

+ + .run(exclude: ['schema_migrations']) ⇒ Object + + + + + +

+ + + + +
+
+
+
+6
+7
+8
+9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+
+
# File 'lib/activerecord-multi-tenant/fast_truncate.rb', line 6
+
+def self.run(exclude: ['schema_migrations'])
+  # This is a slightly faster version of DatabaseCleaner.clean_with(:truncation, pre_count: true)
+  ActiveRecord::Base.connection.execute format(%(
+  DO LANGUAGE plpgsql $$
+  DECLARE
+    t record;
+    tables text[];
+    seq_exists boolean;
+    needs_truncate boolean;
+  BEGIN
+    FOR t IN SELECT schemaname, tablename FROM pg_tables WHERE schemaname = 'public' AND tablename NOT IN (%s) LOOP
+      EXECUTE 'SELECT EXISTS (SELECT * from pg_class c WHERE c.relkind = ''S''
+       AND c.relname=''' || t.tablename || '_id_seq'')' into seq_exists;
+      IF seq_exists THEN
+        EXECUTE 'SELECT is_called FROM ' || t.tablename || '_id_seq' INTO needs_truncate;
+      ELSE
+        needs_truncate := true;
+      END IF;
+
+      IF needs_truncate THEN
+        tables := array_append(tables, quote_ident(t.schemaname) || '.' || quote_ident(t.tablename));
+      END IF;
+    END LOOP;
+
+    IF array_length(tables, 1) > 0 THEN
+      EXECUTE 'TRUNCATE TABLE ' || array_to_string(tables, ', ') || ' RESTART IDENTITY CASCADE';
+    END IF;
+  END$$;), exclude.map { |t| "'#{t}'" }.join('\n'))
+end
+
+
+ +
+ +
+ + + +
+ + \ No newline at end of file diff --git a/docs/source/_static/api-reference/MultiTenant/MigrationExtensions.html b/docs/source/_static/api-reference/MultiTenant/MigrationExtensions.html new file mode 100644 index 00000000..b4292500 --- /dev/null +++ b/docs/source/_static/api-reference/MultiTenant/MigrationExtensions.html @@ -0,0 +1,554 @@ + + + + + + + Module: MultiTenant::MigrationExtensions + + — Documentation by YARD 0.9.34 + + + + + + + + + + + + + + + + + + + +
+ + +

Module: MultiTenant::MigrationExtensions + + + +

+
+ + + + + + + + + + + +
+
Defined in:
+
lib/activerecord-multi-tenant/migrations.rb
+
+ +
+ + + + + + + + + +

+ Instance Method Summary + collapse +

+ + + + + + +
+

Instance Method Details

+ + +
+

+ + #citus_versionObject + + + + + +

+ + + + +
+
+
+
+59
+60
+61
+62
+63
+
+
# File 'lib/activerecord-multi-tenant/migrations.rb', line 59
+
+def citus_version
+  execute("SELECT extversion FROM pg_extension WHERE extname = 'citus'").getvalue(0, 0).try(:split, '-').try(:first)
+rescue ArgumentError => e
+  raise unless e.message == 'invalid tuple number 0'
+end
+
+
+ +
+

+ + #create_distributed_table(table_name, partition_key) ⇒ Object + + + + + +

+ + + + +
+
+
+
+3
+4
+5
+6
+7
+8
+9
+10
+11
+12
+13
+14
+
+
# File 'lib/activerecord-multi-tenant/migrations.rb', line 3
+
+def create_distributed_table(table_name, partition_key)
+  return unless citus_version.present?
+
+  reversible do |dir|
+    dir.up do
+      execute "SELECT create_distributed_table($$#{table_name}$$, $$#{partition_key}$$)"
+    end
+    dir.down do
+      undistribute_table(table_name)
+    end
+  end
+end
+
+
+ +
+

+ + #create_reference_table(table_name) ⇒ Object + + + + + +

+ + + + +
+
+
+
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+
+
# File 'lib/activerecord-multi-tenant/migrations.rb', line 16
+
+def create_reference_table(table_name)
+  return unless citus_version.present?
+
+  reversible do |dir|
+    dir.up do
+      execute "SELECT create_reference_table($$#{table_name}$$)"
+    end
+    dir.down do
+      undistribute_table(table_name)
+    end
+  end
+end
+
+
+ +
+

+ + #enable_extension_on_all_nodes(extension) ⇒ Object + + + + + +

+ + + + +
+
+
+
+55
+56
+57
+
+
# File 'lib/activerecord-multi-tenant/migrations.rb', line 55
+
+def enable_extension_on_all_nodes(extension)
+  execute_on_all_nodes "CREATE EXTENSION IF NOT EXISTS \"#{extension}\""
+end
+
+
+ +
+

+ + #execute_on_all_nodes(sql) ⇒ Object + + + + + +

+ + + + +
+
+
+
+41
+42
+43
+44
+45
+46
+47
+48
+49
+50
+51
+52
+53
+
+
# File 'lib/activerecord-multi-tenant/migrations.rb', line 41
+
+def execute_on_all_nodes(sql)
+  execute sql
+
+  case citus_version
+  when '6.0'
+    execute "SELECT citus_run_on_all_workers($$#{sql}$$)" # initial citus_tools.sql with different names
+  when nil
+    # Do nothing, this is regular Postgres
+  else
+    # 6.1 and newer
+    execute "SELECT run_command_on_workers($$#{sql}$$)"
+  end
+end
+
+
+ +
+

+ + #rebalance_table_shardsObject + + + + + +

+ + + + +
+
+
+
+35
+36
+37
+38
+39
+
+
# File 'lib/activerecord-multi-tenant/migrations.rb', line 35
+
+def rebalance_table_shards
+  return unless citus_version.present?
+
+  execute 'SELECT rebalance_table_shards()'
+end
+
+
+ +
+

+ + #undistribute_table(table_name) ⇒ Object + + + + + +

+ + + + +
+
+
+
+29
+30
+31
+32
+33
+
+
# File 'lib/activerecord-multi-tenant/migrations.rb', line 29
+
+def undistribute_table(table_name)
+  return unless citus_version.present?
+
+  execute "SELECT undistribute_table($$#{table_name}$$))"
+end
+
+
+ +
+ +
+ + + +
+ + \ No newline at end of file diff --git a/docs/source/_static/api-reference/MultiTenant/MissingTenantError.html b/docs/source/_static/api-reference/MultiTenant/MissingTenantError.html new file mode 100644 index 00000000..cff4c2c9 --- /dev/null +++ b/docs/source/_static/api-reference/MultiTenant/MissingTenantError.html @@ -0,0 +1,124 @@ + + + + + + + Exception: MultiTenant::MissingTenantError + + — Documentation by YARD 0.9.34 + + + + + + + + + + + + + + + + + + + +
+ + +

Exception: MultiTenant::MissingTenantError + + + +

+
+ +
+
Inherits:
+
+ StandardError + +
    +
  • Object
  • + + + + + +
+ show all + +
+
+ + + + + + + + + + + +
+
Defined in:
+
lib/activerecord-multi-tenant/multi_tenant.rb
+
+ +
+ + + + + + + + + + + +
+ + + +
+ + \ No newline at end of file diff --git a/docs/source/_static/api-reference/MultiTenant/ModelExtensionsClassMethods.html b/docs/source/_static/api-reference/MultiTenant/ModelExtensionsClassMethods.html new file mode 100644 index 00000000..ccc822f6 --- /dev/null +++ b/docs/source/_static/api-reference/MultiTenant/ModelExtensionsClassMethods.html @@ -0,0 +1,492 @@ + + + + + + + Module: MultiTenant::ModelExtensionsClassMethods + + — Documentation by YARD 0.9.34 + + + + + + + + + + + + + + + + + + + +
+ + +

Module: MultiTenant::ModelExtensionsClassMethods + + + +

+
+ + + + + + + + + + + +
+
Defined in:
+
lib/activerecord-multi-tenant/model_extensions.rb
+
+ +
+ +

Overview

+
+ +

Extension to the model to allow scoping of models to the current tenant. This is done by adding the multitenant method to the models that need to be scoped. This method is called in the model declaration. Adds scoped_by_tenant? partition_key, primary_key and inherited methods to the model

+ + +
+
+
+ + +
+ +

+ Constant Summary + collapse +

+ +
+ +
DEFAULT_ID_FIELD = + +
+
'id'.freeze
+ +
+ + + + + + + + + +

+ Instance Method Summary + collapse +

+ + + + + + +
+

Instance Method Details

+ + +
+

+ + #multi_tenant(tenant_name, options = {}) ⇒ Object + + + + + +

+
+ +

executes when multi_tenant method is called in the model. This method adds the following methods to the model that calls it. scoped_by_tenant? - returns true if the model is scoped by tenant partition_key - returns the partition key for the model primary_key - returns the primary key for the model

+ + +
+
+
+ + +
+ + + + +
+
+
+
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+35
+36
+37
+38
+39
+40
+41
+42
+43
+44
+45
+46
+47
+48
+49
+50
+51
+52
+53
+54
+55
+56
+57
+58
+59
+60
+61
+62
+63
+64
+65
+66
+67
+68
+69
+70
+71
+72
+73
+74
+75
+76
+77
+78
+79
+80
+81
+82
+83
+84
+85
+86
+87
+88
+89
+90
+91
+92
+93
+94
+95
+96
+97
+98
+99
+100
+101
+102
+103
+104
+105
+106
+107
+108
+109
+110
+111
+112
+113
+114
+115
+116
+117
+118
+119
+120
+121
+122
+123
+124
+125
+126
+127
+128
+129
+130
+131
+132
+133
+134
+135
+136
+137
+138
+139
+140
+141
+142
+143
+144
+145
+146
+147
+148
+149
+150
+151
+152
+153
+154
+155
+156
+157
+
+
# File 'lib/activerecord-multi-tenant/model_extensions.rb', line 16
+
+def multi_tenant(tenant_name, options = {})
+  if to_s.underscore.to_sym == tenant_name || (!table_name.nil? && table_name.singularize.to_sym == tenant_name)
+    unless MultiTenant.with_write_only_mode_enabled?
+      # This is the tenant model itself. Workaround for https://github.com/citusdata/citus/issues/687
+      before_create lambda {
+        id = if self.class.columns_hash[self.class.primary_key].type == :uuid
+               SecureRandom.uuid
+             else
+               self.class.connection.select_value(
+                 "SELECT nextval('#{self.class.table_name}_#{self.class.primary_key}_seq'::regclass)"
+               )
+             end
+        self.id ||= id
+      }
+    end
+  else
+    class << self
+      def scoped_by_tenant?
+        true
+      end
+
+      # Allow partition_key to be set from a superclass if not already set in this class
+      def partition_key
+        @partition_key ||= ancestors.detect { |k| k.instance_variable_get(:@partition_key) }
+                                    .try(:instance_variable_get, :@partition_key)
+      end
+
+      # Avoid primary_key errors when using composite primary keys (e.g. id, tenant_id)
+      def primary_key
+        if defined?(PRIMARY_KEY_NOT_SET) ? !PRIMARY_KEY_NOT_SET.equal?(@primary_key) : @primary_key
+          return @primary_key
+        end
+
+        primary_object_keys = Array.wrap(connection.schema_cache.primary_keys(table_name)) - [partition_key]
+
+        @primary_key = if primary_object_keys.size == 1
+                         primary_object_keys.first
+                       elsif connection.schema_cache.columns_hash(table_name).include? DEFAULT_ID_FIELD
+                         DEFAULT_ID_FIELD
+                       end
+      end
+
+      def inherited(subclass)
+        super
+        MultiTenant.register_multi_tenant_model(subclass)
+      end
+    end
+
+    MultiTenant.register_multi_tenant_model(self)
+
+    @partition_key = options[:partition_key] || MultiTenant.partition_key(tenant_name)
+    partition_key = @partition_key
+
+    # Create an implicit belongs_to association only if tenant class exists
+    if MultiTenant.tenant_klass_defined?(tenant_name)
+      belongs_to tenant_name, **options.slice(:class_name, :inverse_of, :optional)
+                                       .merge(foreign_key: options[:partition_key])
+    end
+
+    # New instances should have the tenant set
+    after_initialize proc { |record|
+      if MultiTenant.current_tenant_id &&
+         (!record.attribute_present?(partition_key) || record.public_send(partition_key.to_sym).nil?)
+        record.public_send("#{partition_key}=".to_sym, MultiTenant.current_tenant_id)
+      end
+    }
+
+    # Below block adds the following methods to the model that calls it.
+    # partition_key= - returns the partition key for the model.class << self 'partition' method defined above
+    # is the getter method. Here, there is additional check to assure that the tenant id is not changed once set
+    # tenant_name- returns the name of the tenant model. Its setter and getter methods defined separately
+    # Getter checks for the tenant association and if it is not loaded, returns the current tenant id set
+    # in the MultiTenant module
+    to_include = Module.new do
+      define_method "#{partition_key}=" do |tenant_id|
+        write_attribute(partition_key.to_s, tenant_id)
+
+        # Rails 5 `attribute_will_change!` uses the attribute-method-call rather than `read_attribute`
+        # and will raise ActiveModel::MissingAttributeError if that column was not selected.
+        # This is rescued as NoMethodError and in MRI attribute_was is assigned an arbitrary Object
+        was = send("#{partition_key}_was")
+        was_nil_or_skipped = was.nil? || was.instance_of?(Object)
+
+        if send("#{partition_key}_changed?") && persisted? && !was_nil_or_skipped
+          raise MultiTenant::TenantIsImmutable
+        end
+
+        tenant_id
+      end
+
+      if MultiTenant.tenant_klass_defined?(tenant_name)
+        define_method "#{tenant_name}=" do |model|
+          super(model)
+          if send("#{partition_key}_changed?") && persisted? && !send("#{partition_key}_was").nil?
+            raise MultiTenant::TenantIsImmutable
+          end
+
+          model
+        end
+
+        define_method tenant_name.to_s do
+          if !association(tenant_name.to_sym).loaded? && !MultiTenant.current_tenant_is_id? &&
+             MultiTenant.current_tenant_id && public_send(partition_key) == MultiTenant.current_tenant_id
+            MultiTenant.current_tenant
+          else
+            super()
+          end
+        end
+      end
+    end
+    include to_include
+
+    # Below blocks sets tenant_id for the current session with the tenant_id of the record
+    # If the tenant is not set for the `session.After` the save operation current session tenant is set to nil
+    # If tenant is set for the session, save operation is performed as it is
+    around_save lambda { |record, block|
+      record_tenant = record.attribute_was(partition_key)
+      if persisted? && MultiTenant.current_tenant_id.nil? && !record_tenant.nil?
+        MultiTenant.with(record.public_send(partition_key)) { block.call }
+      else
+        block.call
+      end
+    }
+
+    around_update lambda { |record, block|
+      record_tenant = record.attribute_was(partition_key)
+      if MultiTenant.current_tenant_id.nil? && !record_tenant.nil?
+        MultiTenant.with(record.public_send(partition_key)) { block.call }
+      else
+        block.call
+      end
+    }
+
+    around_destroy lambda { |record, block|
+      if MultiTenant.current_tenant_id.nil?
+        MultiTenant.with(record.public_send(partition_key)) { block.call }
+      else
+        block.call
+      end
+    }
+  end
+end
+
+
+ +
+ +
+ + + +
+ + \ No newline at end of file diff --git a/docs/source/_static/api-reference/MultiTenant/QueryMonitor.html b/docs/source/_static/api-reference/MultiTenant/QueryMonitor.html new file mode 100644 index 00000000..3828b15b --- /dev/null +++ b/docs/source/_static/api-reference/MultiTenant/QueryMonitor.html @@ -0,0 +1,257 @@ + + + + + + + Class: MultiTenant::QueryMonitor + + — Documentation by YARD 0.9.34 + + + + + + + + + + + + + + + + + + + +
+ + +

Class: MultiTenant::QueryMonitor + + + +

+
+ +
+
Inherits:
+
+ Object + +
    +
  • Object
  • + + + +
+ show all + +
+
+ + + + + + + + + + + +
+
Defined in:
+
lib/activerecord-multi-tenant/query_monitor.rb
+
+ +
+ +

Overview

+
+ +

rubocop:enable Style/ClassVars QueryMonitor class to log a warning when a query fails and there is no tenant set start and finish methods are required to be register sql.active_record hook

+ + +
+
+
+ + +
+ + + + + + + +

+ Instance Method Summary + collapse +

+ + + + + + +
+

Instance Method Details

+ + +
+

+ + #finish(_name, _id, payload) ⇒ Object + + + + + +

+ + + + +
+
+
+
+24
+25
+26
+27
+28
+29
+30
+
+
# File 'lib/activerecord-multi-tenant/query_monitor.rb', line 24
+
+def finish(_name, _id, payload)
+  return unless MultiTenant.query_monitor_enabled?
+
+  return unless payload[:exception].present? && MultiTenant.current_tenant_id.nil?
+
+  Rails.logger.info 'WARNING: Tenant not present - make sure to add MultiTenant.with(tenant) { ... }'
+end
+
+
+ +
+

+ + #start(_name, _id, _payload) ⇒ Object + + + + + +

+ + + + +
+
+
+
+22
+
+
# File 'lib/activerecord-multi-tenant/query_monitor.rb', line 22
+
+def start(_name, _id, _payload) end
+
+
+ +
+ +
+ + + +
+ + \ No newline at end of file diff --git a/docs/source/_static/api-reference/MultiTenant/Table.html b/docs/source/_static/api-reference/MultiTenant/Table.html new file mode 100644 index 00000000..bb5c3688 --- /dev/null +++ b/docs/source/_static/api-reference/MultiTenant/Table.html @@ -0,0 +1,419 @@ + + + + + + + Class: MultiTenant::Table + + — Documentation by YARD 0.9.34 + + + + + + + + + + + + + + + + + + + +
+ + +

Class: MultiTenant::Table + + + +

+
+ +
+
Inherits:
+
+ Object + +
    +
  • Object
  • + + + +
+ show all + +
+
+ + + + + + + + + + + +
+
Defined in:
+
lib/activerecord-multi-tenant/query_rewriter.rb
+
+ +
+ + + + + +

Instance Attribute Summary collapse

+ + + + + + +

+ Instance Method Summary + collapse +

+ + + + +
+

Constructor Details

+ +
+

+ + #initialize(arel_table) ⇒ Table + + + + + +

+
+ +

Returns a new instance of Table.

+ + +
+
+
+ + +
+ + + + +
+
+
+
+9
+10
+11
+
+
# File 'lib/activerecord-multi-tenant/query_rewriter.rb', line 9
+
+def initialize(arel_table)
+  @arel_table = arel_table
+end
+
+
+ +
+ +
+

Instance Attribute Details

+ + + +
+

+ + #arel_tableObject (readonly) + + + + + +

+
+ +

Returns the value of attribute arel_table.

+ + +
+
+
+ + +
+ + + + +
+
+
+
+7
+8
+9
+
+
# File 'lib/activerecord-multi-tenant/query_rewriter.rb', line 7
+
+def arel_table
+  @arel_table
+end
+
+
+ +
+ + +
+

Instance Method Details

+ + +
+

+ + #eql?(other) ⇒ Boolean + + + + + +

+
+ + +
+
+
+ +

Returns:

+
    + +
  • + + + (Boolean) + + + +
  • + +
+ +
+ + + + +
+
+
+
+13
+14
+15
+16
+
+
# File 'lib/activerecord-multi-tenant/query_rewriter.rb', line 13
+
+def eql?(other)
+  self.class == other.class &&
+    equality_fields.eql?(other.equality_fields)
+end
+
+
+ +
+

+ + #hashObject + + + + + +

+ + + + +
+
+
+
+18
+19
+20
+
+
# File 'lib/activerecord-multi-tenant/query_rewriter.rb', line 18
+
+def hash
+  equality_fields.hash
+end
+
+
+ +
+ +
+ + + +
+ + \ No newline at end of file diff --git a/docs/source/_static/api-reference/MultiTenant/TenantEnforcementClause.html b/docs/source/_static/api-reference/MultiTenant/TenantEnforcementClause.html new file mode 100644 index 00000000..63815f2a --- /dev/null +++ b/docs/source/_static/api-reference/MultiTenant/TenantEnforcementClause.html @@ -0,0 +1,148 @@ + + + + + + + Class: MultiTenant::TenantEnforcementClause + + — Documentation by YARD 0.9.34 + + + + + + + + + + + + + + + + + + + +
+ + +

Class: MultiTenant::TenantEnforcementClause + + + +

+
+ +
+
Inherits:
+
+ BaseTenantEnforcementClause + + + show all + +
+
+ + + + + + + + + + + +
+
Defined in:
+
lib/activerecord-multi-tenant/query_rewriter.rb
+
+ +
+ + + + + + + +

Instance Attribute Summary

+ +

Attributes inherited from BaseTenantEnforcementClause

+

#tenant_attribute

+ + + + + + + + + +

Method Summary

+ +

Methods inherited from BaseTenantEnforcementClause

+

#initialize, #to_s, #to_sql, #to_str

+ +
+

Constructor Details

+ +

This class inherits a constructor from MultiTenant::BaseTenantEnforcementClause

+ +
+ + +
+ + + +
+ + \ No newline at end of file diff --git a/docs/source/_static/api-reference/MultiTenant/TenantIsImmutable.html b/docs/source/_static/api-reference/MultiTenant/TenantIsImmutable.html new file mode 100644 index 00000000..61390538 --- /dev/null +++ b/docs/source/_static/api-reference/MultiTenant/TenantIsImmutable.html @@ -0,0 +1,135 @@ + + + + + + + Exception: MultiTenant::TenantIsImmutable + + — Documentation by YARD 0.9.34 + + + + + + + + + + + + + + + + + + + +
+ + +

Exception: MultiTenant::TenantIsImmutable + + + +

+
+ +
+
Inherits:
+
+ StandardError + +
    +
  • Object
  • + + + + + +
+ show all + +
+
+ + + + + + + + + + + +
+
Defined in:
+
lib/activerecord-multi-tenant/multi_tenant.rb
+
+ +
+ +

Overview

+
+ +

This exception is raised when a there is an attempt to change tenant

+ + +
+
+
+ + +
+ + + + + + + + + +
+ + + +
+ + \ No newline at end of file diff --git a/docs/source/_static/api-reference/MultiTenant/TenantJoinEnforcementClause.html b/docs/source/_static/api-reference/MultiTenant/TenantJoinEnforcementClause.html new file mode 100644 index 00000000..d16b3457 --- /dev/null +++ b/docs/source/_static/api-reference/MultiTenant/TenantJoinEnforcementClause.html @@ -0,0 +1,310 @@ + + + + + + + Class: MultiTenant::TenantJoinEnforcementClause + + — Documentation by YARD 0.9.34 + + + + + + + + + + + + + + + + + + + +
+ + +

Class: MultiTenant::TenantJoinEnforcementClause + + + +

+
+ +
+
Inherits:
+
+ BaseTenantEnforcementClause + + + show all + +
+
+ + + + + + + + + + + +
+
Defined in:
+
lib/activerecord-multi-tenant/query_rewriter.rb
+
+ +
+ + + + + +

Instance Attribute Summary collapse

+ + + + + + +

Attributes inherited from BaseTenantEnforcementClause

+

#tenant_attribute

+ + + +

+ Instance Method Summary + collapse +

+ + + + + + + + + + + + + +

Methods inherited from BaseTenantEnforcementClause

+

#to_s, #to_sql, #to_str

+ +
+

Constructor Details

+ +
+

+ + #initialize(tenant_attribute, table_left) ⇒ TenantJoinEnforcementClause + + + + + +

+
+ +

Returns a new instance of TenantJoinEnforcementClause.

+ + +
+
+
+ + +
+ + + + +
+
+
+
+215
+216
+217
+218
+219
+
+
# File 'lib/activerecord-multi-tenant/query_rewriter.rb', line 215
+
+def initialize(tenant_attribute, table_left)
+  super(tenant_attribute)
+  @table_left = table_left
+  @model_left = MultiTenant.multi_tenant_model_for_table(table_left.table_name)
+end
+
+
+ +
+ +
+

Instance Attribute Details

+ + + +
+

+ + #table_leftObject (readonly) + + + + + +

+
+ +

Returns the value of attribute table_left.

+ + +
+
+
+ + +
+ + + + +
+
+
+
+213
+214
+215
+
+
# File 'lib/activerecord-multi-tenant/query_rewriter.rb', line 213
+
+def table_left
+  @table_left
+end
+
+
+ +
+ + +
+ + + +
+ + \ No newline at end of file diff --git a/docs/source/_static/api-reference/MultiTenant/TenantValueVisitor.html b/docs/source/_static/api-reference/MultiTenant/TenantValueVisitor.html new file mode 100644 index 00000000..53cd23d9 --- /dev/null +++ b/docs/source/_static/api-reference/MultiTenant/TenantValueVisitor.html @@ -0,0 +1,239 @@ + + + + + + + Module: MultiTenant::TenantValueVisitor + + — Documentation by YARD 0.9.34 + + + + + + + + + + + + + + + + + + + +
+ + +

Module: MultiTenant::TenantValueVisitor + + + +

+
+ + + + + + + + + + + +
+
Defined in:
+
lib/activerecord-multi-tenant/query_rewriter.rb
+
+ +
+ + + + + + + + + +

+ Instance Method Summary + collapse +

+ + + + + + +
+

Instance Method Details

+ + +
+

+ + #visit_MultiTenant_TenantEnforcementClause(obj, collector) ⇒ Object + + + + + +

+
+ +

rubocop:disable Naming/MethodName

+ + +
+
+
+ + +
+ + + + +
+
+
+
+230
+231
+232
+
+
# File 'lib/activerecord-multi-tenant/query_rewriter.rb', line 230
+
+def visit_MultiTenant_TenantEnforcementClause(obj, collector)
+  collector << obj
+end
+
+
+ +
+

+ + #visit_MultiTenant_TenantJoinEnforcementClause(obj, collector) ⇒ Object + + + + + +

+ + + + +
+
+
+
+234
+235
+236
+
+
# File 'lib/activerecord-multi-tenant/query_rewriter.rb', line 234
+
+def visit_MultiTenant_TenantJoinEnforcementClause(obj, collector)
+  collector << obj
+end
+
+
+ +
+ +
+ + + +
+ + \ No newline at end of file diff --git a/docs/source/_static/api-reference/MultiTenantFindBy.html b/docs/source/_static/api-reference/MultiTenantFindBy.html new file mode 100644 index 00000000..40ad0650 --- /dev/null +++ b/docs/source/_static/api-reference/MultiTenantFindBy.html @@ -0,0 +1,180 @@ + + + + + + + Module: MultiTenantFindBy + + — Documentation by YARD 0.9.34 + + + + + + + + + + + + + + + + + + + +
+ + +

Module: MultiTenantFindBy + + + +

+
+ + + + + + + + + + + +
+
Defined in:
+
lib/activerecord-multi-tenant/query_rewriter.rb
+
+ +
+ + + + + + + + + +

+ Instance Method Summary + collapse +

+ + + + + + +
+

Instance Method Details

+ + +
+

+ + #cached_find_by_statement(key, &block) ⇒ Object + + + + + +

+ + + + +
+
+
+
+375
+376
+377
+378
+379
+380
+
+
# File 'lib/activerecord-multi-tenant/query_rewriter.rb', line 375
+
+def cached_find_by_statement(key, &block)
+  return super unless respond_to?(:scoped_by_tenant?) && scoped_by_tenant?
+
+  key = Array.wrap(key) + [MultiTenant.current_tenant_id.to_s]
+  super(key, &block)
+end
+
+
+ +
+ +
+ + + +
+ + \ No newline at end of file diff --git a/docs/source/_static/api-reference/Sidekiq.html b/docs/source/_static/api-reference/Sidekiq.html new file mode 100644 index 00000000..5ecbd375 --- /dev/null +++ b/docs/source/_static/api-reference/Sidekiq.html @@ -0,0 +1,126 @@ + + + + + + + Module: Sidekiq + + — Documentation by YARD 0.9.34 + + + + + + + + + + + + + + + + + + + +
+ + +

Module: Sidekiq + + + +

+
+ + + + + + + + + + + +
+
Defined in:
+
lib/activerecord-multi-tenant/sidekiq.rb
+
+ +
+ +

Overview

+
+ +

Bulk push support for Sidekiq while setting multi-tenant information. This is a copy of the Sidekiq::Client#push_bulk method with the addition of setting the multi-tenant information for each job.

+ + +
+
+
+ + +

Defined Under Namespace

+

+ + + + + Classes: Client + + +

+ + + + + + + + + +
+ + + +
+ + \ No newline at end of file diff --git a/docs/source/_static/api-reference/Sidekiq/Client.html b/docs/source/_static/api-reference/Sidekiq/Client.html new file mode 100644 index 00000000..1a18841b --- /dev/null +++ b/docs/source/_static/api-reference/Sidekiq/Client.html @@ -0,0 +1,302 @@ + + + + + + + Class: Sidekiq::Client + + — Documentation by YARD 0.9.34 + + + + + + + + + + + + + + + + + + + +
+ + +

Class: Sidekiq::Client + + + +

+
+ +
+
Inherits:
+
+ Object + +
    +
  • Object
  • + + + +
+ show all + +
+
+ + + + + + + + + + + +
+
Defined in:
+
lib/activerecord-multi-tenant/sidekiq.rb
+
+ +
+ + + + + + + + + +

+ Class Method Summary + collapse +

+ + + +

+ Instance Method Summary + collapse +

+ + + + + + +
+

Class Method Details

+ + +
+

+ + .push_bulk_with_tenants(items) ⇒ Object + + + + + +

+ + + + +
+
+
+
+84
+85
+86
+
+
# File 'lib/activerecord-multi-tenant/sidekiq.rb', line 84
+
+def push_bulk_with_tenants(items)
+  new.push_bulk_with_tenants(items)
+end
+
+
+ +
+ +
+

Instance Method Details

+ + +
+

+ + #push_bulk_with_tenants(items) ⇒ Object + + + + + +

+
+ +

Allows the caller to enqueue multiple Sidekiq jobs with tenant information in a single call. It ensures that each job is processed within the correct tenant context and returns an array of job IDs for the enqueued jobs

+ + +
+
+
+ + +
+ + + + +
+
+
+
+62
+63
+64
+65
+66
+67
+68
+69
+70
+71
+72
+73
+74
+75
+76
+77
+78
+79
+80
+
+
# File 'lib/activerecord-multi-tenant/sidekiq.rb', line 62
+
+def push_bulk_with_tenants(items)
+  first_job = items['jobs'].first
+  return [] unless first_job # no jobs to push
+  unless first_job.is_a?(Hash)
+    raise ArgumentError, "Bulk arguments must be an Array of Hashes: [{ 'args' => [1], 'tenant_id' => 1 }, ...]"
+  end
+
+  normed = normalize_item(items.except('jobs').merge('args' => []))
+  payloads = items['jobs'].map do |job|
+    MultiTenant.with(job['tenant_id']) do
+      copy = normed.merge('args' => job['args'], 'jid' => SecureRandom.hex(12), 'enqueued_at' => Time.now.to_f)
+      result = process_single(items['class'], copy)
+      result || nil
+    end
+  end.compact
+
+  raw_push(payloads) unless payloads.empty?
+  payloads.collect { |payload| payload['jid'] }
+end
+
+
+ +
+ +
+ + + +
+ + \ No newline at end of file diff --git a/docs/source/_static/api-reference/Sidekiq/Middleware/MultiTenant.html b/docs/source/_static/api-reference/Sidekiq/Middleware/MultiTenant.html new file mode 100644 index 00000000..e6b45f6b --- /dev/null +++ b/docs/source/_static/api-reference/Sidekiq/Middleware/MultiTenant.html @@ -0,0 +1,126 @@ + + + + + + + Module: Sidekiq::Middleware::MultiTenant + + — Documentation by YARD 0.9.34 + + + + + + + + + + + + + + + + + + + +
+ + +

Module: Sidekiq::Middleware::MultiTenant + + + +

+
+ + + + + + + + + + + +
+
Defined in:
+
lib/activerecord-multi-tenant/sidekiq.rb
+
+ +
+ +

Overview

+
+ +

Adds methods to handle tenant information both in the client and server.

+ + +
+
+
+ + +

Defined Under Namespace

+

+ + + + + Classes: Client, Server + + +

+ + + + + + + + + +
+ + + +
+ + \ No newline at end of file diff --git a/docs/source/_static/api-reference/Sidekiq/Middleware/MultiTenant/Client.html b/docs/source/_static/api-reference/Sidekiq/Middleware/MultiTenant/Client.html new file mode 100644 index 00000000..bb036f05 --- /dev/null +++ b/docs/source/_static/api-reference/Sidekiq/Middleware/MultiTenant/Client.html @@ -0,0 +1,217 @@ + + + + + + + Class: Sidekiq::Middleware::MultiTenant::Client + + — Documentation by YARD 0.9.34 + + + + + + + + + + + + + + + + + + + +
+ + +

Class: Sidekiq::Middleware::MultiTenant::Client + + + +

+
+ +
+
Inherits:
+
+ Object + +
    +
  • Object
  • + + + +
+ show all + +
+
+ + + + + + + + + + + +
+
Defined in:
+
lib/activerecord-multi-tenant/sidekiq.rb
+
+ +
+ +

Overview

+
+ +

Get the current tenant and store in the message to be sent to Sidekiq.

+ + +
+
+
+ + +
+ + + + + + + +

+ Instance Method Summary + collapse +

+ + + + + + +
+

Instance Method Details

+ + +
+

+ + #call(_worker_class, msg, _queue, _redis_pool) ⇒ Object + + + + + +

+ + + + +
+
+
+
+7
+8
+9
+10
+11
+12
+13
+14
+15
+16
+17
+
+
# File 'lib/activerecord-multi-tenant/sidekiq.rb', line 7
+
+def call(_worker_class, msg, _queue, _redis_pool)
+  if MultiTenant.current_tenant.present?
+    msg['multi_tenant'] ||=
+      {
+        'class' => MultiTenant.current_tenant_class,
+        'id' => MultiTenant.current_tenant_id
+      }
+  end
+
+  yield
+end
+
+
+ +
+ +
+ + + +
+ + \ No newline at end of file diff --git a/docs/source/_static/api-reference/Sidekiq/Middleware/MultiTenant/Server.html b/docs/source/_static/api-reference/Sidekiq/Middleware/MultiTenant/Server.html new file mode 100644 index 00000000..c2283eaf --- /dev/null +++ b/docs/source/_static/api-reference/Sidekiq/Middleware/MultiTenant/Server.html @@ -0,0 +1,219 @@ + + + + + + + Class: Sidekiq::Middleware::MultiTenant::Server + + — Documentation by YARD 0.9.34 + + + + + + + + + + + + + + + + + + + +
+ + +

Class: Sidekiq::Middleware::MultiTenant::Server + + + +

+
+ +
+
Inherits:
+
+ Object + +
    +
  • Object
  • + + + +
+ show all + +
+
+ + + + + + + + + + + +
+
Defined in:
+
lib/activerecord-multi-tenant/sidekiq.rb
+
+ +
+ +

Overview

+
+ +

Pull the tenant out and run the current thread with it.

+ + +
+
+
+ + +
+ + + + + + + +

+ Instance Method Summary + collapse +

+ + + + + + +
+

Instance Method Details

+ + +
+

+ + #call(_worker_class, msg, _queue, &block) ⇒ Object + + + + + +

+ + + + +
+
+
+
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+
+
# File 'lib/activerecord-multi-tenant/sidekiq.rb', line 22
+
+def call(_worker_class, msg, _queue, &block)
+  if msg.key?('multi_tenant')
+    tenant = begin
+      msg['multi_tenant']['class'].constantize.find(msg['multi_tenant']['id'])
+    rescue ActiveRecord::RecordNotFound
+      msg['multi_tenant']['id']
+    end
+    MultiTenant.with(tenant, &block)
+  else
+    yield
+  end
+end
+
+
+ +
+ +
+ + + +
+ + \ No newline at end of file diff --git a/docs/source/_static/api-reference/_index.html b/docs/source/_static/api-reference/_index.html new file mode 100644 index 00000000..ffaea9a3 --- /dev/null +++ b/docs/source/_static/api-reference/_index.html @@ -0,0 +1,399 @@ + + + + + + + Documentation by YARD 0.9.34 + + + + + + + + + + + + + + + + + + + +
+ + +

Documentation by YARD 0.9.34

+
+

Alphabetic Index

+ +

File Listing

+ + +
+

Namespace Listing A-Z

+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+ + + +
+ + \ No newline at end of file diff --git a/docs/source/_static/api-reference/class_list.html b/docs/source/_static/api-reference/class_list.html new file mode 100644 index 00000000..c959f4e0 --- /dev/null +++ b/docs/source/_static/api-reference/class_list.html @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + Class List + + + +
+
+

Class List

+ + + +
+ + +
+ + diff --git a/docs/source/_static/api-reference/css/common.css b/docs/source/_static/api-reference/css/common.css new file mode 100644 index 00000000..cf25c452 --- /dev/null +++ b/docs/source/_static/api-reference/css/common.css @@ -0,0 +1 @@ +/* Override this file with custom rules */ \ No newline at end of file diff --git a/docs/source/_static/api-reference/css/full_list.css b/docs/source/_static/api-reference/css/full_list.css new file mode 100644 index 00000000..fa359824 --- /dev/null +++ b/docs/source/_static/api-reference/css/full_list.css @@ -0,0 +1,58 @@ +body { + margin: 0; + font-family: "Lucida Sans", "Lucida Grande", Verdana, Arial, sans-serif; + font-size: 13px; + height: 101%; + overflow-x: hidden; + background: #fafafa; +} + +h1 { padding: 12px 10px; padding-bottom: 0; margin: 0; font-size: 1.4em; } +.clear { clear: both; } +.fixed_header { position: fixed; background: #fff; width: 100%; padding-bottom: 10px; margin-top: 0; top: 0; z-index: 9999; height: 70px; } +#search { position: absolute; right: 5px; top: 9px; padding-left: 24px; } +#content.insearch #search, #content.insearch #noresults { background: url(data:image/gif;base64,R0lGODlhEAAQAPYAAP///wAAAPr6+pKSkoiIiO7u7sjIyNjY2J6engAAAI6OjsbGxjIyMlJSUuzs7KamppSUlPLy8oKCghwcHLKysqSkpJqamvT09Pj4+KioqM7OzkRERAwMDGBgYN7e3ujo6Ly8vCoqKjY2NkZGRtTU1MTExDw8PE5OTj4+PkhISNDQ0MrKylpaWrS0tOrq6nBwcKysrLi4uLq6ul5eXlxcXGJiYoaGhuDg4H5+fvz8/KKiohgYGCwsLFZWVgQEBFBQUMzMzDg4OFhYWBoaGvDw8NbW1pycnOLi4ubm5kBAQKqqqiQkJCAgIK6urnJyckpKSjQ0NGpqatLS0sDAwCYmJnx8fEJCQlRUVAoKCggICLCwsOTk5ExMTPb29ra2tmZmZmhoaNzc3KCgoBISEiIiIgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH/C05FVFNDQVBFMi4wAwEAAAAh/hpDcmVhdGVkIHdpdGggYWpheGxvYWQuaW5mbwAh+QQJCAAAACwAAAAAEAAQAAAHaIAAgoMgIiYlg4kACxIaACEJCSiKggYMCRselwkpghGJBJEcFgsjJyoAGBmfggcNEx0flBiKDhQFlIoCCA+5lAORFb4AJIihCRbDxQAFChAXw9HSqb60iREZ1omqrIPdJCTe0SWI09GBACH5BAkIAAAALAAAAAAQABAAAAdrgACCgwc0NTeDiYozCQkvOTo9GTmDKy8aFy+NOBA7CTswgywJDTIuEjYFIY0JNYMtKTEFiRU8Pjwygy4ws4owPyCKwsMAJSTEgiQlgsbIAMrO0dKDGMTViREZ14kYGRGK38nHguHEJcvTyIEAIfkECQgAAAAsAAAAABAAEAAAB2iAAIKDAggPg4iJAAMJCRUAJRIqiRGCBI0WQEEJJkWDERkYAAUKEBc4Po1GiKKJHkJDNEeKig4URLS0ICImJZAkuQAhjSi/wQyNKcGDCyMnk8u5rYrTgqDVghgZlYjcACTA1sslvtHRgQAh+QQJCAAAACwAAAAAEAAQAAAHZ4AAgoOEhYaCJSWHgxGDJCQARAtOUoQRGRiFD0kJUYWZhUhKT1OLhR8wBaaFBzQ1NwAlkIszCQkvsbOHL7Y4q4IuEjaqq0ZQD5+GEEsJTDCMmIUhtgk1lo6QFUwJVDKLiYJNUd6/hoEAIfkECQgAAAAsAAAAABAAEAAAB2iAAIKDhIWGgiUlh4MRgyQkjIURGRiGGBmNhJWHm4uen4ICCA+IkIsDCQkVACWmhwSpFqAABQoQF6ALTkWFnYMrVlhWvIKTlSAiJiVVPqlGhJkhqShHV1lCW4cMqSkAR1ofiwsjJyqGgQAh+QQJCAAAACwAAAAAEAAQAAAHZ4AAgoOEhYaCJSWHgxGDJCSMhREZGIYYGY2ElYebi56fhyWQniSKAKKfpaCLFlAPhl0gXYNGEwkhGYREUywag1wJwSkHNDU3D0kJYIMZQwk8MjPBLx9eXwuETVEyAC/BOKsuEjYFhoEAIfkECQgAAAAsAAAAABAAEAAAB2eAAIKDhIWGgiUlh4MRgyQkjIURGRiGGBmNhJWHm4ueICImip6CIQkJKJ4kigynKaqKCyMnKqSEK05StgAGQRxPYZaENqccFgIID4KXmQBhXFkzDgOnFYLNgltaSAAEpxa7BQoQF4aBACH5BAkIAAAALAAAAAAQABAAAAdogACCg4SFggJiPUqCJSWGgkZjCUwZACQkgxGEXAmdT4UYGZqCGWQ+IjKGGIUwPzGPhAc0NTewhDOdL7Ykji+dOLuOLhI2BbaFETICx4MlQitdqoUsCQ2vhKGjglNfU0SWmILaj43M5oEAOwAAAAAAAAAAAA==) no-repeat center left; } +#full_list { padding: 0; list-style: none; margin-left: 0; margin-top: 80px; font-size: 1.1em; } +#full_list ul { padding: 0; } +#full_list li { padding: 0; margin: 0; list-style: none; } +#full_list li .item { padding: 5px 5px 5px 12px; } +#noresults { padding: 7px 12px; background: #fff; } +#content.insearch #noresults { margin-left: 7px; } +li.collapsed ul { display: none; } +li a.toggle { cursor: default; position: relative; left: -5px; top: 4px; text-indent: -999px; width: 10px; height: 9px; margin-left: -10px; display: block; float: left; background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAASCAYAAABb0P4QAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAK8AAACvABQqw0mAAAABx0RVh0U29mdHdhcmUAQWRvYmUgRmlyZXdvcmtzIENTM5jWRgMAAAAVdEVYdENyZWF0aW9uIFRpbWUAMy8xNC8wOeNZPpQAAAE2SURBVDiNrZTBccIwEEXfelIAHUA6CZ24BGaWO+FuzZAK4k6gg5QAdGAq+Bxs2Yqx7BzyL7Llp/VfzZeQhCTc/ezuGzKKnKSzpCxXJM8fwNXda3df5RZETlIt6YUzSQDs93sl8w3wBZxCCE10GM1OcWbWjB2mWgEH4Mfdyxm3PSepBHibgQE2wLe7r4HjEidpnXMYdQPKEMJcsZ4zs2POYQOcaPfwMVOo58zsAdMt18BuoVDPxUJRacELbXv3hUIX2vYmOUvi8C8ydz/ThjXrqKqqLbDIAdsCKBd+Wo7GWa7o9qzOQHVVVXeAbs+yHHCH4aTsaCOQqunmUy1yBUAXkdMIfMlgF5EXLo2OpV/c/Up7jG4hhHcYLgWzAZXUc2b2ixsfvc/RmNNfOXD3Q/oeL9axJE1yT9IOoUu6MGUkAAAAAElFTkSuQmCC) no-repeat bottom left; } +li.collapsed a.toggle { opacity: 0.5; cursor: default; background-position: top left; } +li { color: #888; cursor: pointer; } +li.deprecated { text-decoration: line-through; font-style: italic; } +li.odd { background: #f0f0f0; } +li.even { background: #fafafa; } +.item:hover { background: #ddd; } +li small:before { content: "("; } +li small:after { content: ")"; } +li small.search_info { display: none; } +a, a:visited { text-decoration: none; color: #05a; } +li.clicked > .item { background: #05a; color: #ccc; } +li.clicked > .item a, li.clicked > .item a:visited { color: #eee; } +li.clicked > .item a.toggle { opacity: 0.5; background-position: bottom right; } +li.collapsed.clicked a.toggle { background-position: top right; } +#search input { border: 1px solid #bbb; border-radius: 3px; } +#full_list_nav { margin-left: 10px; font-size: 0.9em; display: block; color: #aaa; } +#full_list_nav a, #nav a:visited { color: #358; } +#full_list_nav a:hover { background: transparent; color: #5af; } +#full_list_nav span:after { content: ' | '; } +#full_list_nav span:last-child:after { content: ''; } + +#content h1 { margin-top: 0; } +li { white-space: nowrap; cursor: normal; } +li small { display: block; font-size: 0.8em; } +li small:before { content: ""; } +li small:after { content: ""; } +li small.search_info { display: none; } +#search { width: 170px; position: static; margin: 3px; margin-left: 10px; font-size: 0.9em; color: #888; padding-left: 0; padding-right: 24px; } +#content.insearch #search { background-position: center right; } +#search input { width: 110px; } + +#full_list.insearch ul { display: block; } +#full_list.insearch .item { display: none; } +#full_list.insearch .found { display: block; padding-left: 11px !important; } +#full_list.insearch li a.toggle { display: none; } +#full_list.insearch li small.search_info { display: block; } diff --git a/docs/source/_static/api-reference/css/style.css b/docs/source/_static/api-reference/css/style.css new file mode 100644 index 00000000..eb0dbc86 --- /dev/null +++ b/docs/source/_static/api-reference/css/style.css @@ -0,0 +1,497 @@ +html { + width: 100%; + height: 100%; +} +body { + font-family: "Lucida Sans", "Lucida Grande", Verdana, Arial, sans-serif; + font-size: 13px; + width: 100%; + margin: 0; + padding: 0; + display: flex; + display: -webkit-flex; + display: -ms-flexbox; +} + +#nav { + position: relative; + width: 100%; + height: 100%; + border: 0; + border-right: 1px dotted #eee; + overflow: auto; +} +.nav_wrap { + margin: 0; + padding: 0; + width: 20%; + height: 100%; + position: relative; + display: flex; + display: -webkit-flex; + display: -ms-flexbox; + flex-shrink: 0; + -webkit-flex-shrink: 0; + -ms-flex: 1 0; +} +#resizer { + position: absolute; + right: -5px; + top: 0; + width: 10px; + height: 100%; + cursor: col-resize; + z-index: 9999; +} +#main { + flex: 5 1; + -webkit-flex: 5 1; + -ms-flex: 5 1; + outline: none; + position: relative; + background: #fff; + padding: 1.2em; + padding-top: 0.2em; + box-sizing: border-box; +} + +@media (max-width: 920px) { + .nav_wrap { width: 100%; top: 0; right: 0; overflow: visible; position: absolute; } + #resizer { display: none; } + #nav { + z-index: 9999; + background: #fff; + display: none; + position: absolute; + top: 40px; + right: 12px; + width: 500px; + max-width: 80%; + height: 80%; + overflow-y: scroll; + border: 1px solid #999; + border-collapse: collapse; + box-shadow: -7px 5px 25px #aaa; + border-radius: 2px; + } +} + +@media (min-width: 920px) { + body { height: 100%; overflow: hidden; } + #main { height: 100%; overflow: auto; } + #search { display: none; } +} + +#main img { max-width: 100%; } +h1 { font-size: 25px; margin: 1em 0 0.5em; padding-top: 4px; border-top: 1px dotted #d5d5d5; } +h1.noborder { border-top: 0px; margin-top: 0; padding-top: 4px; } +h1.title { margin-bottom: 10px; } +h1.alphaindex { margin-top: 0; font-size: 22px; } +h2 { + padding: 0; + padding-bottom: 3px; + border-bottom: 1px #aaa solid; + font-size: 1.4em; + margin: 1.8em 0 0.5em; + position: relative; +} +h2 small { font-weight: normal; font-size: 0.7em; display: inline; position: absolute; right: 0; } +h2 small a { + display: block; + height: 20px; + border: 1px solid #aaa; + border-bottom: 0; + border-top-left-radius: 5px; + background: #f8f8f8; + position: relative; + padding: 2px 7px; +} +.clear { clear: both; } +.inline { display: inline; } +.inline p:first-child { display: inline; } +.docstring, .tags, #filecontents { font-size: 15px; line-height: 1.5145em; } +.docstring p > code, .docstring p > tt, .tags p > code, .tags p > tt { + color: #c7254e; background: #f9f2f4; padding: 2px 4px; font-size: 1em; + border-radius: 4px; +} +.docstring h1, .docstring h2, .docstring h3, .docstring h4 { padding: 0; border: 0; border-bottom: 1px dotted #bbb; } +.docstring h1 { font-size: 1.2em; } +.docstring h2 { font-size: 1.1em; } +.docstring h3, .docstring h4 { font-size: 1em; border-bottom: 0; padding-top: 10px; } +.summary_desc .object_link a, .docstring .object_link a { + font-family: monospace; font-size: 1.05em; + color: #05a; background: #EDF4FA; padding: 2px 4px; font-size: 1em; + border-radius: 4px; +} +.rdoc-term { padding-right: 25px; font-weight: bold; } +.rdoc-list p { margin: 0; padding: 0; margin-bottom: 4px; } +.summary_desc pre.code .object_link a, .docstring pre.code .object_link a { + padding: 0px; background: inherit; color: inherit; border-radius: inherit; +} + +/* style for */ +#filecontents table, .docstring table { border-collapse: collapse; } +#filecontents table th, #filecontents table td, +.docstring table th, .docstring table td { border: 1px solid #ccc; padding: 8px; padding-right: 17px; } +#filecontents table tr:nth-child(odd), +.docstring table tr:nth-child(odd) { background: #eee; } +#filecontents table tr:nth-child(even), +.docstring table tr:nth-child(even) { background: #fff; } +#filecontents table th, .docstring table th { background: #fff; } + +/* style for
a",d=q.getElementsByTagName("*"),e=q.getElementsByTagName("a")[0];if(!d||!d.length||!e)return{};g=c.createElement("select"),h=g.appendChild(c.createElement("option")),i=q.getElementsByTagName("input")[0],b={leadingWhitespace:q.firstChild.nodeType===3,tbody:!q.getElementsByTagName("tbody").length,htmlSerialize:!!q.getElementsByTagName("link").length,style:/top/.test(e.getAttribute("style")),hrefNormalized:e.getAttribute("href")==="/a",opacity:/^0.55/.test(e.style.opacity),cssFloat:!!e.style.cssFloat,checkOn:i.value==="on",optSelected:h.selected,getSetAttribute:q.className!=="t",enctype:!!c.createElement("form").enctype,html5Clone:c.createElement("nav").cloneNode(!0).outerHTML!=="<:nav>",submitBubbles:!0,changeBubbles:!0,focusinBubbles:!1,deleteExpando:!0,noCloneEvent:!0,inlineBlockNeedsLayout:!1,shrinkWrapBlocks:!1,reliableMarginRight:!0},i.checked=!0,b.noCloneChecked=i.cloneNode(!0).checked,g.disabled=!0,b.optDisabled=!h.disabled;try{delete q.test}catch(s){b.deleteExpando=!1}!q.addEventListener&&q.attachEvent&&q.fireEvent&&(q.attachEvent("onclick",function(){b.noCloneEvent=!1}),q.cloneNode(!0).fireEvent("onclick")),i=c.createElement("input"),i.value="t",i.setAttribute("type","radio"),b.radioValue=i.value==="t",i.setAttribute("checked","checked"),q.appendChild(i),k=c.createDocumentFragment(),k.appendChild(q.lastChild),b.checkClone=k.cloneNode(!0).cloneNode(!0).lastChild.checked,b.appendChecked=i.checked,k.removeChild(i),k.appendChild(q),q.innerHTML="",a.getComputedStyle&&(j=c.createElement("div"),j.style.width="0",j.style.marginRight="0",q.style.width="2px",q.appendChild(j),b.reliableMarginRight=(parseInt((a.getComputedStyle(j,null)||{marginRight:0}).marginRight,10)||0)===0);if(q.attachEvent)for(o in{submit:1,change:1,focusin:1})n="on"+o,p=n in q,p||(q.setAttribute(n,"return;"),p=typeof q[n]=="function"),b[o+"Bubbles"]=p;k.removeChild(q),k=g=h=j=q=i=null,f(function(){var a,d,e,g,h,i,j,k,m,n,o,r=c.getElementsByTagName("body")[0];!r||(j=1,k="position:absolute;top:0;left:0;width:1px;height:1px;margin:0;",m="visibility:hidden;border:0;",n="style='"+k+"border:5px solid #000;padding:0;'",o="
"+""+"
",a=c.createElement("div"),a.style.cssText=m+"width:0;height:0;position:static;top:0;margin-top:"+j+"px",r.insertBefore(a,r.firstChild),q=c.createElement("div"),a.appendChild(q),q.innerHTML="
t
",l=q.getElementsByTagName("td"),p=l[0].offsetHeight===0,l[0].style.display="",l[1].style.display="none",b.reliableHiddenOffsets=p&&l[0].offsetHeight===0,q.innerHTML="",q.style.width=q.style.paddingLeft="1px",f.boxModel=b.boxModel=q.offsetWidth===2,typeof q.style.zoom!="undefined"&&(q.style.display="inline",q.style.zoom=1,b.inlineBlockNeedsLayout=q.offsetWidth===2,q.style.display="",q.innerHTML="
",b.shrinkWrapBlocks=q.offsetWidth!==2),q.style.cssText=k+m,q.innerHTML=o,d=q.firstChild,e=d.firstChild,h=d.nextSibling.firstChild.firstChild,i={doesNotAddBorder:e.offsetTop!==5,doesAddBorderForTableAndCells:h.offsetTop===5},e.style.position="fixed",e.style.top="20px",i.fixedPosition=e.offsetTop===20||e.offsetTop===15,e.style.position=e.style.top="",d.style.overflow="hidden",d.style.position="relative",i.subtractsBorderForOverflowNotVisible=e.offsetTop===-5,i.doesNotIncludeMarginInBodyOffset=r.offsetTop!==j,r.removeChild(a),q=a=null,f.extend(b,i))});return b}();var j=/^(?:\{.*\}|\[.*\])$/,k=/([A-Z])/g;f.extend({cache:{},uuid:0,expando:"jQuery"+(f.fn.jquery+Math.random()).replace(/\D/g,""),noData:{embed:!0,object:"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000",applet:!0},hasData:function(a){a=a.nodeType?f.cache[a[f.expando]]:a[f.expando];return!!a&&!m(a)},data:function(a,c,d,e){if(!!f.acceptData(a)){var g,h,i,j=f.expando,k=typeof c=="string",l=a.nodeType,m=l?f.cache:a,n=l?a[j]:a[j]&&j,o=c==="events";if((!n||!m[n]||!o&&!e&&!m[n].data)&&k&&d===b)return;n||(l?a[j]=n=++f.uuid:n=j),m[n]||(m[n]={},l||(m[n].toJSON=f.noop));if(typeof c=="object"||typeof c=="function")e?m[n]=f.extend(m[n],c):m[n].data=f.extend(m[n].data,c);g=h=m[n],e||(h.data||(h.data={}),h=h.data),d!==b&&(h[f.camelCase(c)]=d);if(o&&!h[c])return g.events;k?(i=h[c],i==null&&(i=h[f.camelCase(c)])):i=h;return i}},removeData:function(a,b,c){if(!!f.acceptData(a)){var d,e,g,h=f.expando,i=a.nodeType,j=i?f.cache:a,k=i?a[h]:h;if(!j[k])return;if(b){d=c?j[k]:j[k].data;if(d){f.isArray(b)||(b in d?b=[b]:(b=f.camelCase(b),b in d?b=[b]:b=b.split(" ")));for(e=0,g=b.length;e-1)return!0;return!1},val:function(a){var c,d,e,g=this[0];{if(!!arguments.length){e=f.isFunction(a);return this.each(function(d){var g=f(this),h;if(this.nodeType===1){e?h=a.call(this,d,g.val()):h=a,h==null?h="":typeof h=="number"?h+="":f.isArray(h)&&(h=f.map(h,function(a){return a==null?"":a+""})),c=f.valHooks[this.nodeName.toLowerCase()]||f.valHooks[this.type];if(!c||!("set"in c)||c.set(this,h,"value")===b)this.value=h}})}if(g){c=f.valHooks[g.nodeName.toLowerCase()]||f.valHooks[g.type];if(c&&"get"in c&&(d=c.get(g,"value"))!==b)return d;d=g.value;return typeof d=="string"?d.replace(q,""):d==null?"":d}}}}),f.extend({valHooks:{option:{get:function(a){var b=a.attributes.value;return!b||b.specified?a.value:a.text}},select:{get:function(a){var b,c,d,e,g=a.selectedIndex,h=[],i=a.options,j=a.type==="select-one";if(g<0)return null;c=j?g:0,d=j?g+1:i.length;for(;c=0}),c.length||(a.selectedIndex=-1);return c}}},attrFn:{val:!0,css:!0,html:!0,text:!0,data:!0,width:!0,height:!0,offset:!0},attr:function(a,c,d,e){var g,h,i,j=a.nodeType;if(!!a&&j!==3&&j!==8&&j!==2){if(e&&c in f.attrFn)return f(a)[c](d);if(typeof a.getAttribute=="undefined")return f.prop(a,c,d);i=j!==1||!f.isXMLDoc(a),i&&(c=c.toLowerCase(),h=f.attrHooks[c]||(u.test(c)?x:w));if(d!==b){if(d===null){f.removeAttr(a,c);return}if(h&&"set"in h&&i&&(g=h.set(a,d,c))!==b)return g;a.setAttribute(c,""+d);return d}if(h&&"get"in h&&i&&(g=h.get(a,c))!==null)return g;g=a.getAttribute(c);return g===null?b:g}},removeAttr:function(a,b){var c,d,e,g,h=0;if(b&&a.nodeType===1){d=b.toLowerCase().split(p),g=d.length;for(;h=0}})});var z=/^(?:textarea|input|select)$/i,A=/^([^\.]*)?(?:\.(.+))?$/,B=/\bhover(\.\S+)?\b/,C=/^key/,D=/^(?:mouse|contextmenu)|click/,E=/^(?:focusinfocus|focusoutblur)$/,F=/^(\w*)(?:#([\w\-]+))?(?:\.([\w\-]+))?$/,G=function(a){var b=F.exec(a);b&&(b[1]=(b[1]||"").toLowerCase(),b[3]=b[3]&&new RegExp("(?:^|\\s)"+b[3]+"(?:\\s|$)"));return b},H=function(a,b){var c=a.attributes||{};return(!b[1]||a.nodeName.toLowerCase()===b[1])&&(!b[2]||(c.id||{}).value===b[2])&&(!b[3]||b[3].test((c["class"]||{}).value))},I=function(a){return f.event.special.hover?a:a.replace(B,"mouseenter$1 mouseleave$1")}; +f.event={add:function(a,c,d,e,g){var h,i,j,k,l,m,n,o,p,q,r,s;if(!(a.nodeType===3||a.nodeType===8||!c||!d||!(h=f._data(a)))){d.handler&&(p=d,d=p.handler),d.guid||(d.guid=f.guid++),j=h.events,j||(h.events=j={}),i=h.handle,i||(h.handle=i=function(a){return typeof f!="undefined"&&(!a||f.event.triggered!==a.type)?f.event.dispatch.apply(i.elem,arguments):b},i.elem=a),c=f.trim(I(c)).split(" ");for(k=0;k=0&&(h=h.slice(0,-1),k=!0),h.indexOf(".")>=0&&(i=h.split("."),h=i.shift(),i.sort());if((!e||f.event.customEvent[h])&&!f.event.global[h])return;c=typeof c=="object"?c[f.expando]?c:new f.Event(h,c):new f.Event(h),c.type=h,c.isTrigger=!0,c.exclusive=k,c.namespace=i.join("."),c.namespace_re=c.namespace?new RegExp("(^|\\.)"+i.join("\\.(?:.*\\.)?")+"(\\.|$)"):null,o=h.indexOf(":")<0?"on"+h:"";if(!e){j=f.cache;for(l in j)j[l].events&&j[l].events[h]&&f.event.trigger(c,d,j[l].handle.elem,!0);return}c.result=b,c.target||(c.target=e),d=d!=null?f.makeArray(d):[],d.unshift(c),p=f.event.special[h]||{};if(p.trigger&&p.trigger.apply(e,d)===!1)return;r=[[e,p.bindType||h]];if(!g&&!p.noBubble&&!f.isWindow(e)){s=p.delegateType||h,m=E.test(s+h)?e:e.parentNode,n=null;for(;m;m=m.parentNode)r.push([m,s]),n=m;n&&n===e.ownerDocument&&r.push([n.defaultView||n.parentWindow||a,s])}for(l=0;le&&i.push({elem:this,matches:d.slice(e)});for(j=0;j0?this.on(b,null,a,c):this.trigger(b)},f.attrFn&&(f.attrFn[b]=!0),C.test(b)&&(f.event.fixHooks[b]=f.event.keyHooks),D.test(b)&&(f.event.fixHooks[b]=f.event.mouseHooks)}),function(){function x(a,b,c,e,f,g){for(var h=0,i=e.length;h0){k=j;break}}j=j[a]}e[h]=k}}}function w(a,b,c,e,f,g){for(var h=0,i=e.length;h+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g,d="sizcache"+(Math.random()+"").replace(".",""),e=0,g=Object.prototype.toString,h=!1,i=!0,j=/\\/g,k=/\r\n/g,l=/\W/;[0,0].sort(function(){i=!1;return 0});var m=function(b,d,e,f){e=e||[],d=d||c;var h=d;if(d.nodeType!==1&&d.nodeType!==9)return[];if(!b||typeof b!="string")return e;var i,j,k,l,n,q,r,t,u=!0,v=m.isXML(d),w=[],x=b;do{a.exec(""),i=a.exec(x);if(i){x=i[3],w.push(i[1]);if(i[2]){l=i[3];break}}}while(i);if(w.length>1&&p.exec(b))if(w.length===2&&o.relative[w[0]])j=y(w[0]+w[1],d,f);else{j=o.relative[w[0]]?[d]:m(w.shift(),d);while(w.length)b=w.shift(),o.relative[b]&&(b+=w.shift()),j=y(b,j,f)}else{!f&&w.length>1&&d.nodeType===9&&!v&&o.match.ID.test(w[0])&&!o.match.ID.test(w[w.length-1])&&(n=m.find(w.shift(),d,v),d=n.expr?m.filter(n.expr,n.set)[0]:n.set[0]);if(d){n=f?{expr:w.pop(),set:s(f)}:m.find(w.pop(),w.length===1&&(w[0]==="~"||w[0]==="+")&&d.parentNode?d.parentNode:d,v),j=n.expr?m.filter(n.expr,n.set):n.set,w.length>0?k=s(j):u=!1;while(w.length)q=w.pop(),r=q,o.relative[q]?r=w.pop():q="",r==null&&(r=d),o.relative[q](k,r,v)}else k=w=[]}k||(k=j),k||m.error(q||b);if(g.call(k)==="[object Array]")if(!u)e.push.apply(e,k);else if(d&&d.nodeType===1)for(t=0;k[t]!=null;t++)k[t]&&(k[t]===!0||k[t].nodeType===1&&m.contains(d,k[t]))&&e.push(j[t]);else for(t=0;k[t]!=null;t++)k[t]&&k[t].nodeType===1&&e.push(j[t]);else s(k,e);l&&(m(l,h,e,f),m.uniqueSort(e));return e};m.uniqueSort=function(a){if(u){h=i,a.sort(u);if(h)for(var b=1;b0},m.find=function(a,b,c){var d,e,f,g,h,i;if(!a)return[];for(e=0,f=o.order.length;e":function(a,b){var c,d=typeof b=="string",e=0,f=a.length;if(d&&!l.test(b)){b=b.toLowerCase();for(;e=0)?c||d.push(h):c&&(b[g]=!1));return!1},ID:function(a){return a[1].replace(j,"")},TAG:function(a,b){return a[1].replace(j,"").toLowerCase()},CHILD:function(a){if(a[1]==="nth"){a[2]||m.error(a[0]),a[2]=a[2].replace(/^\+|\s*/g,"");var b=/(-?)(\d*)(?:n([+\-]?\d*))?/.exec(a[2]==="even"&&"2n"||a[2]==="odd"&&"2n+1"||!/\D/.test(a[2])&&"0n+"+a[2]||a[2]);a[2]=b[1]+(b[2]||1)-0,a[3]=b[3]-0}else a[2]&&m.error(a[0]);a[0]=e++;return a},ATTR:function(a,b,c,d,e,f){var g=a[1]=a[1].replace(j,"");!f&&o.attrMap[g]&&(a[1]=o.attrMap[g]),a[4]=(a[4]||a[5]||"").replace(j,""),a[2]==="~="&&(a[4]=" "+a[4]+" ");return a},PSEUDO:function(b,c,d,e,f){if(b[1]==="not")if((a.exec(b[3])||"").length>1||/^\w/.test(b[3]))b[3]=m(b[3],null,null,c);else{var g=m.filter(b[3],c,d,!0^f);d||e.push.apply(e,g);return!1}else if(o.match.POS.test(b[0])||o.match.CHILD.test(b[0]))return!0;return b},POS:function(a){a.unshift(!0);return a}},filters:{enabled:function(a){return a.disabled===!1&&a.type!=="hidden"},disabled:function(a){return a.disabled===!0},checked:function(a){return a.checked===!0},selected:function(a){a.parentNode&&a.parentNode.selectedIndex;return a.selected===!0},parent:function(a){return!!a.firstChild},empty:function(a){return!a.firstChild},has:function(a,b,c){return!!m(c[3],a).length},header:function(a){return/h\d/i.test(a.nodeName)},text:function(a){var b=a.getAttribute("type"),c=a.type;return a.nodeName.toLowerCase()==="input"&&"text"===c&&(b===c||b===null)},radio:function(a){return a.nodeName.toLowerCase()==="input"&&"radio"===a.type},checkbox:function(a){return a.nodeName.toLowerCase()==="input"&&"checkbox"===a.type},file:function(a){return a.nodeName.toLowerCase()==="input"&&"file"===a.type},password:function(a){return a.nodeName.toLowerCase()==="input"&&"password"===a.type},submit:function(a){var b=a.nodeName.toLowerCase();return(b==="input"||b==="button")&&"submit"===a.type},image:function(a){return a.nodeName.toLowerCase()==="input"&&"image"===a.type},reset:function(a){var b=a.nodeName.toLowerCase();return(b==="input"||b==="button")&&"reset"===a.type},button:function(a){var b=a.nodeName.toLowerCase();return b==="input"&&"button"===a.type||b==="button"},input:function(a){return/input|select|textarea|button/i.test(a.nodeName)},focus:function(a){return a===a.ownerDocument.activeElement}},setFilters:{first:function(a,b){return b===0},last:function(a,b,c,d){return b===d.length-1},even:function(a,b){return b%2===0},odd:function(a,b){return b%2===1},lt:function(a,b,c){return bc[3]-0},nth:function(a,b,c){return c[3]-0===b},eq:function(a,b,c){return c[3]-0===b}},filter:{PSEUDO:function(a,b,c,d){var e=b[1],f=o.filters[e];if(f)return f(a,c,b,d);if(e==="contains")return(a.textContent||a.innerText||n([a])||"").indexOf(b[3])>=0;if(e==="not"){var g=b[3];for(var h=0,i=g.length;h=0}},ID:function(a,b){return a.nodeType===1&&a.getAttribute("id")===b},TAG:function(a,b){return b==="*"&&a.nodeType===1||!!a.nodeName&&a.nodeName.toLowerCase()===b},CLASS:function(a,b){return(" "+(a.className||a.getAttribute("class"))+" ").indexOf(b)>-1},ATTR:function(a,b){var c=b[1],d=m.attr?m.attr(a,c):o.attrHandle[c]?o.attrHandle[c](a):a[c]!=null?a[c]:a.getAttribute(c),e=d+"",f=b[2],g=b[4];return d==null?f==="!=":!f&&m.attr?d!=null:f==="="?e===g:f==="*="?e.indexOf(g)>=0:f==="~="?(" "+e+" ").indexOf(g)>=0:g?f==="!="?e!==g:f==="^="?e.indexOf(g)===0:f==="$="?e.substr(e.length-g.length)===g:f==="|="?e===g||e.substr(0,g.length+1)===g+"-":!1:e&&d!==!1},POS:function(a,b,c,d){var e=b[2],f=o.setFilters[e];if(f)return f(a,c,b,d)}}},p=o.match.POS,q=function(a,b){return"\\"+(b-0+1)};for(var r in o.match)o.match[r]=new RegExp(o.match[r].source+/(?![^\[]*\])(?![^\(]*\))/.source),o.leftMatch[r]=new RegExp(/(^(?:.|\r|\n)*?)/.source+o.match[r].source.replace(/\\(\d+)/g,q));var s=function(a,b){a=Array.prototype.slice.call(a,0);if(b){b.push.apply(b,a);return b}return a};try{Array.prototype.slice.call(c.documentElement.childNodes,0)[0].nodeType}catch(t){s=function(a,b){var c=0,d=b||[];if(g.call(a)==="[object Array]")Array.prototype.push.apply(d,a);else if(typeof a.length=="number")for(var e=a.length;c",e.insertBefore(a,e.firstChild),c.getElementById(d)&&(o.find.ID=function(a,c,d){if(typeof c.getElementById!="undefined"&&!d){var e=c.getElementById(a[1]);return e?e.id===a[1]||typeof e.getAttributeNode!="undefined"&&e.getAttributeNode("id").nodeValue===a[1]?[e]:b:[]}},o.filter.ID=function(a,b){var c=typeof a.getAttributeNode!="undefined"&&a.getAttributeNode("id");return a.nodeType===1&&c&&c.nodeValue===b}),e.removeChild(a),e=a=null}(),function(){var a=c.createElement("div");a.appendChild(c.createComment("")),a.getElementsByTagName("*").length>0&&(o.find.TAG=function(a,b){var c=b.getElementsByTagName(a[1]);if(a[1]==="*"){var d=[];for(var e=0;c[e];e++)c[e].nodeType===1&&d.push(c[e]);c=d}return c}),a.innerHTML="",a.firstChild&&typeof a.firstChild.getAttribute!="undefined"&&a.firstChild.getAttribute("href")!=="#"&&(o.attrHandle.href=function(a){return a.getAttribute("href",2)}),a=null}(),c.querySelectorAll&&function(){var a=m,b=c.createElement("div"),d="__sizzle__";b.innerHTML="

";if(!b.querySelectorAll||b.querySelectorAll(".TEST").length!==0){m=function(b,e,f,g){e=e||c;if(!g&&!m.isXML(e)){var h=/^(\w+$)|^\.([\w\-]+$)|^#([\w\-]+$)/.exec(b);if(h&&(e.nodeType===1||e.nodeType===9)){if(h[1])return s(e.getElementsByTagName(b),f);if(h[2]&&o.find.CLASS&&e.getElementsByClassName)return s(e.getElementsByClassName(h[2]),f)}if(e.nodeType===9){if(b==="body"&&e.body)return s([e.body],f);if(h&&h[3]){var i=e.getElementById(h[3]);if(!i||!i.parentNode)return s([],f);if(i.id===h[3])return s([i],f)}try{return s(e.querySelectorAll(b),f)}catch(j){}}else if(e.nodeType===1&&e.nodeName.toLowerCase()!=="object"){var k=e,l=e.getAttribute("id"),n=l||d,p=e.parentNode,q=/^\s*[+~]/.test(b);l?n=n.replace(/'/g,"\\$&"):e.setAttribute("id",n),q&&p&&(e=e.parentNode);try{if(!q||p)return s(e.querySelectorAll("[id='"+n+"'] "+b),f)}catch(r){}finally{l||k.removeAttribute("id")}}}return a(b,e,f,g)};for(var e in a)m[e]=a[e];b=null}}(),function(){var a=c.documentElement,b=a.matchesSelector||a.mozMatchesSelector||a.webkitMatchesSelector||a.msMatchesSelector;if(b){var d=!b.call(c.createElement("div"),"div"),e=!1;try{b.call(c.documentElement,"[test!='']:sizzle")}catch(f){e=!0}m.matchesSelector=function(a,c){c=c.replace(/\=\s*([^'"\]]*)\s*\]/g,"='$1']");if(!m.isXML(a))try{if(e||!o.match.PSEUDO.test(c)&&!/!=/.test(c)){var f=b.call(a,c);if(f||!d||a.document&&a.document.nodeType!==11)return f}}catch(g){}return m(c,null,null,[a]).length>0}}}(),function(){var a=c.createElement("div");a.innerHTML="
";if(!!a.getElementsByClassName&&a.getElementsByClassName("e").length!==0){a.lastChild.className="e";if(a.getElementsByClassName("e").length===1)return;o.order.splice(1,0,"CLASS"),o.find.CLASS=function(a,b,c){if(typeof b.getElementsByClassName!="undefined"&&!c)return b.getElementsByClassName(a[1])},a=null}}(),c.documentElement.contains?m.contains=function(a,b){return a!==b&&(a.contains?a.contains(b):!0)}:c.documentElement.compareDocumentPosition?m.contains=function(a,b){return!!(a.compareDocumentPosition(b)&16)}:m.contains=function(){return!1},m.isXML=function(a){var b=(a?a.ownerDocument||a:0).documentElement;return b?b.nodeName!=="HTML":!1};var y=function(a,b,c){var d,e=[],f="",g=b.nodeType?[b]:b;while(d=o.match.PSEUDO.exec(a))f+=d[0],a=a.replace(o.match.PSEUDO,"");a=o.relative[a]?a+"*":a;for(var h=0,i=g.length;h0)for(h=g;h=0:f.filter(a,this).length>0:this.filter(a).length>0)},closest:function(a,b){var c=[],d,e,g=this[0];if(f.isArray(a)){var h=1;while(g&&g.ownerDocument&&g!==b){for(d=0;d-1:f.find.matchesSelector(g,a)){c.push(g);break}g=g.parentNode;if(!g||!g.ownerDocument||g===b||g.nodeType===11)break}}c=c.length>1?f.unique(c):c;return this.pushStack(c,"closest",a)},index:function(a){if(!a)return this[0]&&this[0].parentNode?this.prevAll().length:-1;if(typeof a=="string")return f.inArray(this[0],f(a));return f.inArray(a.jquery?a[0]:a,this)},add:function(a,b){var c=typeof a=="string"?f(a,b):f.makeArray(a&&a.nodeType?[a]:a),d=f.merge(this.get(),c);return this.pushStack(S(c[0])||S(d[0])?d:f.unique(d))},andSelf:function(){return this.add(this.prevObject)}}),f.each({parent:function(a){var b=a.parentNode;return b&&b.nodeType!==11?b:null},parents:function(a){return f.dir(a,"parentNode")},parentsUntil:function(a,b,c){return f.dir(a,"parentNode",c)},next:function(a){return f.nth(a,2,"nextSibling")},prev:function(a){return f.nth(a,2,"previousSibling")},nextAll:function(a){return f.dir(a,"nextSibling")},prevAll:function(a){return f.dir(a,"previousSibling")},nextUntil:function(a,b,c){return f.dir(a,"nextSibling",c)},prevUntil:function(a,b,c){return f.dir(a,"previousSibling",c)},siblings:function(a){return f.sibling(a.parentNode.firstChild,a)},children:function(a){return f.sibling(a.firstChild)},contents:function(a){return f.nodeName(a,"iframe")?a.contentDocument||a.contentWindow.document:f.makeArray(a.childNodes)}},function(a,b){f.fn[a]=function(c,d){var e=f.map(this,b,c);L.test(a)||(d=c),d&&typeof d=="string"&&(e=f.filter(d,e)),e=this.length>1&&!R[a]?f.unique(e):e,(this.length>1||N.test(d))&&M.test(a)&&(e=e.reverse());return this.pushStack(e,a,P.call(arguments).join(","))}}),f.extend({filter:function(a,b,c){c&&(a=":not("+a+")");return b.length===1?f.find.matchesSelector(b[0],a)?[b[0]]:[]:f.find.matches(a,b)},dir:function(a,c,d){var e=[],g=a[c];while(g&&g.nodeType!==9&&(d===b||g.nodeType!==1||!f(g).is(d)))g.nodeType===1&&e.push(g),g=g[c];return e},nth:function(a,b,c,d){b=b||1;var e=0;for(;a;a=a[c])if(a.nodeType===1&&++e===b)break;return a},sibling:function(a,b){var c=[];for(;a;a=a.nextSibling)a.nodeType===1&&a!==b&&c.push(a);return c}});var V="abbr|article|aside|audio|canvas|datalist|details|figcaption|figure|footer|header|hgroup|mark|meter|nav|output|progress|section|summary|time|video",W=/ jQuery\d+="(?:\d+|null)"/g,X=/^\s+/,Y=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/ig,Z=/<([\w:]+)/,$=/",""],legend:[1,"
","
"],thead:[1,"","
"],tr:[2,"","
"],td:[3,"","
"],col:[2,"","
"],area:[1,"",""],_default:[0,"",""]},bh=U(c);bg.optgroup=bg.option,bg.tbody=bg.tfoot=bg.colgroup=bg.caption=bg.thead,bg.th=bg.td,f.support.htmlSerialize||(bg._default=[1,"div
","
"]),f.fn.extend({text:function(a){if(f.isFunction(a))return this.each(function(b){var c=f(this);c.text(a.call(this,b,c.text()))});if(typeof a!="object"&&a!==b)return this.empty().append((this[0]&&this[0].ownerDocument||c).createTextNode(a));return f.text(this)},wrapAll:function(a){if(f.isFunction(a))return this.each(function(b){f(this).wrapAll(a.call(this,b))});if(this[0]){var b=f(a,this[0].ownerDocument).eq(0).clone(!0);this[0].parentNode&&b.insertBefore(this[0]),b.map(function(){var a=this;while(a.firstChild&&a.firstChild.nodeType===1)a=a.firstChild;return a}).append(this)}return this},wrapInner:function(a){if(f.isFunction(a))return this.each(function(b){f(this).wrapInner(a.call(this,b))});return this.each(function(){var b=f(this),c=b.contents();c.length?c.wrapAll(a):b.append(a)})},wrap:function(a){var b=f.isFunction(a);return this.each(function(c){f(this).wrapAll(b?a.call(this,c):a)})},unwrap:function(){return this.parent().each(function(){f.nodeName(this,"body")||f(this).replaceWith(this.childNodes)}).end()},append:function(){return this.domManip(arguments,!0,function(a){this.nodeType===1&&this.appendChild(a)})},prepend:function(){return this.domManip(arguments,!0,function(a){this.nodeType===1&&this.insertBefore(a,this.firstChild)})},before:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,!1,function(a){this.parentNode.insertBefore(a,this)});if(arguments.length){var a=f.clean(arguments);a.push.apply(a,this.toArray());return this.pushStack(a,"before",arguments)}},after:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,!1,function(a){this.parentNode.insertBefore(a,this.nextSibling)});if(arguments.length){var a=this.pushStack(this,"after",arguments);a.push.apply(a,f.clean(arguments));return a}},remove:function(a,b){for(var c=0,d;(d=this[c])!=null;c++)if(!a||f.filter(a,[d]).length)!b&&d.nodeType===1&&(f.cleanData(d.getElementsByTagName("*")),f.cleanData([d])),d.parentNode&&d.parentNode.removeChild(d);return this},empty:function() +{for(var a=0,b;(b=this[a])!=null;a++){b.nodeType===1&&f.cleanData(b.getElementsByTagName("*"));while(b.firstChild)b.removeChild(b.firstChild)}return this},clone:function(a,b){a=a==null?!1:a,b=b==null?a:b;return this.map(function(){return f.clone(this,a,b)})},html:function(a){if(a===b)return this[0]&&this[0].nodeType===1?this[0].innerHTML.replace(W,""):null;if(typeof a=="string"&&!ba.test(a)&&(f.support.leadingWhitespace||!X.test(a))&&!bg[(Z.exec(a)||["",""])[1].toLowerCase()]){a=a.replace(Y,"<$1>");try{for(var c=0,d=this.length;c1&&l0?this.clone(!0):this).get();f(e[h])[b](j),d=d.concat(j)}return this.pushStack(d,a,e.selector)}}),f.extend({clone:function(a,b,c){var d,e,g,h=f.support.html5Clone||!bc.test("<"+a.nodeName)?a.cloneNode(!0):bo(a);if((!f.support.noCloneEvent||!f.support.noCloneChecked)&&(a.nodeType===1||a.nodeType===11)&&!f.isXMLDoc(a)){bk(a,h),d=bl(a),e=bl(h);for(g=0;d[g];++g)e[g]&&bk(d[g],e[g])}if(b){bj(a,h);if(c){d=bl(a),e=bl(h);for(g=0;d[g];++g)bj(d[g],e[g])}}d=e=null;return h},clean:function(a,b,d,e){var g;b=b||c,typeof b.createElement=="undefined"&&(b=b.ownerDocument||b[0]&&b[0].ownerDocument||c);var h=[],i;for(var j=0,k;(k=a[j])!=null;j++){typeof k=="number"&&(k+="");if(!k)continue;if(typeof k=="string")if(!_.test(k))k=b.createTextNode(k);else{k=k.replace(Y,"<$1>");var l=(Z.exec(k)||["",""])[1].toLowerCase(),m=bg[l]||bg._default,n=m[0],o=b.createElement("div");b===c?bh.appendChild(o):U(b).appendChild(o),o.innerHTML=m[1]+k+m[2];while(n--)o=o.lastChild;if(!f.support.tbody){var p=$.test(k),q=l==="table"&&!p?o.firstChild&&o.firstChild.childNodes:m[1]===""&&!p?o.childNodes:[];for(i=q.length-1;i>=0;--i)f.nodeName(q[i],"tbody")&&!q[i].childNodes.length&&q[i].parentNode.removeChild(q[i])}!f.support.leadingWhitespace&&X.test(k)&&o.insertBefore(b.createTextNode(X.exec(k)[0]),o.firstChild),k=o.childNodes}var r;if(!f.support.appendChecked)if(k[0]&&typeof (r=k.length)=="number")for(i=0;i=0)return b+"px"}}}),f.support.opacity||(f.cssHooks.opacity={get:function(a,b){return br.test((b&&a.currentStyle?a.currentStyle.filter:a.style.filter)||"")?parseFloat(RegExp.$1)/100+"":b?"1":""},set:function(a,b){var c=a.style,d=a.currentStyle,e=f.isNumeric(b)?"alpha(opacity="+b*100+")":"",g=d&&d.filter||c.filter||"";c.zoom=1;if(b>=1&&f.trim(g.replace(bq,""))===""){c.removeAttribute("filter");if(d&&!d.filter)return}c.filter=bq.test(g)?g.replace(bq,e):g+" "+e}}),f(function(){f.support.reliableMarginRight||(f.cssHooks.marginRight={get:function(a,b){var c;f.swap(a,{display:"inline-block"},function(){b?c=bz(a,"margin-right","marginRight"):c=a.style.marginRight});return c}})}),c.defaultView&&c.defaultView.getComputedStyle&&(bA=function(a,b){var c,d,e;b=b.replace(bs,"-$1").toLowerCase(),(d=a.ownerDocument.defaultView)&&(e=d.getComputedStyle(a,null))&&(c=e.getPropertyValue(b),c===""&&!f.contains(a.ownerDocument.documentElement,a)&&(c=f.style(a,b)));return c}),c.documentElement.currentStyle&&(bB=function(a,b){var c,d,e,f=a.currentStyle&&a.currentStyle[b],g=a.style;f===null&&g&&(e=g[b])&&(f=e),!bt.test(f)&&bu.test(f)&&(c=g.left,d=a.runtimeStyle&&a.runtimeStyle.left,d&&(a.runtimeStyle.left=a.currentStyle.left),g.left=b==="fontSize"?"1em":f||0,f=g.pixelLeft+"px",g.left=c,d&&(a.runtimeStyle.left=d));return f===""?"auto":f}),bz=bA||bB,f.expr&&f.expr.filters&&(f.expr.filters.hidden=function(a){var b=a.offsetWidth,c=a.offsetHeight;return b===0&&c===0||!f.support.reliableHiddenOffsets&&(a.style&&a.style.display||f.css(a,"display"))==="none"},f.expr.filters.visible=function(a){return!f.expr.filters.hidden(a)});var bD=/%20/g,bE=/\[\]$/,bF=/\r?\n/g,bG=/#.*$/,bH=/^(.*?):[ \t]*([^\r\n]*)\r?$/mg,bI=/^(?:color|date|datetime|datetime-local|email|hidden|month|number|password|range|search|tel|text|time|url|week)$/i,bJ=/^(?:about|app|app\-storage|.+\-extension|file|res|widget):$/,bK=/^(?:GET|HEAD)$/,bL=/^\/\//,bM=/\?/,bN=/)<[^<]*)*<\/script>/gi,bO=/^(?:select|textarea)/i,bP=/\s+/,bQ=/([?&])_=[^&]*/,bR=/^([\w\+\.\-]+:)(?:\/\/([^\/?#:]*)(?::(\d+))?)?/,bS=f.fn.load,bT={},bU={},bV,bW,bX=["*/"]+["*"];try{bV=e.href}catch(bY){bV=c.createElement("a"),bV.href="",bV=bV.href}bW=bR.exec(bV.toLowerCase())||[],f.fn.extend({load:function(a,c,d){if(typeof a!="string"&&bS)return bS.apply(this,arguments);if(!this.length)return this;var e=a.indexOf(" ");if(e>=0){var g=a.slice(e,a.length);a=a.slice(0,e)}var h="GET";c&&(f.isFunction(c)?(d=c,c=b):typeof c=="object"&&(c=f.param(c,f.ajaxSettings.traditional),h="POST"));var i=this;f.ajax({url:a,type:h,dataType:"html",data:c,complete:function(a,b,c){c=a.responseText,a.isResolved()&&(a.done(function(a){c=a}),i.html(g?f("
").append(c.replace(bN,"")).find(g):c)),d&&i.each(d,[c,b,a])}});return this},serialize:function(){return f.param(this.serializeArray())},serializeArray:function(){return this.map(function(){return this.elements?f.makeArray(this.elements):this}).filter(function(){return this.name&&!this.disabled&&(this.checked||bO.test(this.nodeName)||bI.test(this.type))}).map(function(a,b){var c=f(this).val();return c==null?null:f.isArray(c)?f.map(c,function(a,c){return{name:b.name,value:a.replace(bF,"\r\n")}}):{name:b.name,value:c.replace(bF,"\r\n")}}).get()}}),f.each("ajaxStart ajaxStop ajaxComplete ajaxError ajaxSuccess ajaxSend".split(" "),function(a,b){f.fn[b]=function(a){return this.on(b,a)}}),f.each(["get","post"],function(a,c){f[c]=function(a,d,e,g){f.isFunction(d)&&(g=g||e,e=d,d=b);return f.ajax({type:c,url:a,data:d,success:e,dataType:g})}}),f.extend({getScript:function(a,c){return f.get(a,b,c,"script")},getJSON:function(a,b,c){return f.get(a,b,c,"json")},ajaxSetup:function(a,b){b?b_(a,f.ajaxSettings):(b=a,a=f.ajaxSettings),b_(a,b);return a},ajaxSettings:{url:bV,isLocal:bJ.test(bW[1]),global:!0,type:"GET",contentType:"application/x-www-form-urlencoded",processData:!0,async:!0,accepts:{xml:"application/xml, text/xml",html:"text/html",text:"text/plain",json:"application/json, text/javascript","*":bX},contents:{xml:/xml/,html:/html/,json:/json/},responseFields:{xml:"responseXML",text:"responseText"},converters:{"* text":a.String,"text html":!0,"text json":f.parseJSON,"text xml":f.parseXML},flatOptions:{context:!0,url:!0}},ajaxPrefilter:bZ(bT),ajaxTransport:bZ(bU),ajax:function(a,c){function w(a,c,l,m){if(s!==2){s=2,q&&clearTimeout(q),p=b,n=m||"",v.readyState=a>0?4:0;var o,r,u,w=c,x=l?cb(d,v,l):b,y,z;if(a>=200&&a<300||a===304){if(d.ifModified){if(y=v.getResponseHeader("Last-Modified"))f.lastModified[k]=y;if(z=v.getResponseHeader("Etag"))f.etag[k]=z}if(a===304)w="notmodified",o=!0;else try{r=cc(d,x),w="success",o=!0}catch(A){w="parsererror",u=A}}else{u=w;if(!w||a)w="error",a<0&&(a=0)}v.status=a,v.statusText=""+(c||w),o?h.resolveWith(e,[r,w,v]):h.rejectWith(e,[v,w,u]),v.statusCode(j),j=b,t&&g.trigger("ajax"+(o?"Success":"Error"),[v,d,o?r:u]),i.fireWith(e,[v,w]),t&&(g.trigger("ajaxComplete",[v,d]),--f.active||f.event.trigger("ajaxStop"))}}typeof a=="object"&&(c=a,a=b),c=c||{};var d=f.ajaxSetup({},c),e=d.context||d,g=e!==d&&(e.nodeType||e instanceof f)?f(e):f.event,h=f.Deferred(),i=f.Callbacks("once memory"),j=d.statusCode||{},k,l={},m={},n,o,p,q,r,s=0,t,u,v={readyState:0,setRequestHeader:function(a,b){if(!s){var c=a.toLowerCase();a=m[c]=m[c]||a,l[a]=b}return this},getAllResponseHeaders:function(){return s===2?n:null},getResponseHeader:function(a){var c;if(s===2){if(!o){o={};while(c=bH.exec(n))o[c[1].toLowerCase()]=c[2]}c=o[a.toLowerCase()]}return c===b?null:c},overrideMimeType:function(a){s||(d.mimeType=a);return this},abort:function(a){a=a||"abort",p&&p.abort(a),w(0,a);return this}};h.promise(v),v.success=v.done,v.error=v.fail,v.complete=i.add,v.statusCode=function(a){if(a){var b;if(s<2)for(b in a)j[b]=[j[b],a[b]];else b=a[v.status],v.then(b,b)}return this},d.url=((a||d.url)+"").replace(bG,"").replace(bL,bW[1]+"//"),d.dataTypes=f.trim(d.dataType||"*").toLowerCase().split(bP),d.crossDomain==null&&(r=bR.exec(d.url.toLowerCase()),d.crossDomain=!(!r||r[1]==bW[1]&&r[2]==bW[2]&&(r[3]||(r[1]==="http:"?80:443))==(bW[3]||(bW[1]==="http:"?80:443)))),d.data&&d.processData&&typeof d.data!="string"&&(d.data=f.param(d.data,d.traditional)),b$(bT,d,c,v);if(s===2)return!1;t=d.global,d.type=d.type.toUpperCase(),d.hasContent=!bK.test(d.type),t&&f.active++===0&&f.event.trigger("ajaxStart");if(!d.hasContent){d.data&&(d.url+=(bM.test(d.url)?"&":"?")+d.data,delete d.data),k=d.url;if(d.cache===!1){var x=f.now(),y=d.url.replace(bQ,"$1_="+x);d.url=y+(y===d.url?(bM.test(d.url)?"&":"?")+"_="+x:"")}}(d.data&&d.hasContent&&d.contentType!==!1||c.contentType)&&v.setRequestHeader("Content-Type",d.contentType),d.ifModified&&(k=k||d.url,f.lastModified[k]&&v.setRequestHeader("If-Modified-Since",f.lastModified[k]),f.etag[k]&&v.setRequestHeader("If-None-Match",f.etag[k])),v.setRequestHeader("Accept",d.dataTypes[0]&&d.accepts[d.dataTypes[0]]?d.accepts[d.dataTypes[0]]+(d.dataTypes[0]!=="*"?", "+bX+"; q=0.01":""):d.accepts["*"]);for(u in d.headers)v.setRequestHeader(u,d.headers[u]);if(d.beforeSend&&(d.beforeSend.call(e,v,d)===!1||s===2)){v.abort();return!1}for(u in{success:1,error:1,complete:1})v[u](d[u]);p=b$(bU,d,c,v);if(!p)w(-1,"No Transport");else{v.readyState=1,t&&g.trigger("ajaxSend",[v,d]),d.async&&d.timeout>0&&(q=setTimeout(function(){v.abort("timeout")},d.timeout));try{s=1,p.send(l,w)}catch(z){if(s<2)w(-1,z);else throw z}}return v},param:function(a,c){var d=[],e=function(a,b){b=f.isFunction(b)?b():b,d[d.length]=encodeURIComponent(a)+"="+encodeURIComponent(b)};c===b&&(c=f.ajaxSettings.traditional);if(f.isArray(a)||a.jquery&&!f.isPlainObject(a))f.each(a,function(){e(this.name,this.value)});else for(var g in a)ca(g,a[g],c,e);return d.join("&").replace(bD,"+")}}),f.extend({active:0,lastModified:{},etag:{}});var cd=f.now(),ce=/(\=)\?(&|$)|\?\?/i;f.ajaxSetup({jsonp:"callback",jsonpCallback:function(){return f.expando+"_"+cd++}}),f.ajaxPrefilter("json jsonp",function(b,c,d){var e=b.contentType==="application/x-www-form-urlencoded"&&typeof b.data=="string";if(b.dataTypes[0]==="jsonp"||b.jsonp!==!1&&(ce.test(b.url)||e&&ce.test(b.data))){var g,h=b.jsonpCallback=f.isFunction(b.jsonpCallback)?b.jsonpCallback():b.jsonpCallback,i=a[h],j=b.url,k=b.data,l="$1"+h+"$2";b.jsonp!==!1&&(j=j.replace(ce,l),b.url===j&&(e&&(k=k.replace(ce,l)),b.data===k&&(j+=(/\?/.test(j)?"&":"?")+b.jsonp+"="+h))),b.url=j,b.data=k,a[h]=function(a){g=[a]},d.always(function(){a[h]=i,g&&f.isFunction(i)&&a[h](g[0])}),b.converters["script json"]=function(){g||f.error(h+" was not called");return g[0]},b.dataTypes[0]="json";return"script"}}),f.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/javascript|ecmascript/},converters:{"text script":function(a){f.globalEval(a);return a}}}),f.ajaxPrefilter("script",function(a){a.cache===b&&(a.cache=!1),a.crossDomain&&(a.type="GET",a.global=!1)}),f.ajaxTransport("script",function(a){if(a.crossDomain){var d,e=c.head||c.getElementsByTagName("head")[0]||c.documentElement;return{send:function(f,g){d=c.createElement("script"),d.async="async",a.scriptCharset&&(d.charset=a.scriptCharset),d.src=a.url,d.onload=d.onreadystatechange=function(a,c){if(c||!d.readyState||/loaded|complete/.test(d.readyState))d.onload=d.onreadystatechange=null,e&&d.parentNode&&e.removeChild(d),d=b,c||g(200,"success")},e.insertBefore(d,e.firstChild)},abort:function(){d&&d.onload(0,1)}}}});var cf=a.ActiveXObject?function(){for(var a in ch)ch[a](0,1)}:!1,cg=0,ch;f.ajaxSettings.xhr=a.ActiveXObject?function(){return!this.isLocal&&ci()||cj()}:ci,function(a){f.extend(f.support,{ajax:!!a,cors:!!a&&"withCredentials"in a})}(f.ajaxSettings.xhr()),f.support.ajax&&f.ajaxTransport(function(c){if(!c.crossDomain||f.support.cors){var d;return{send:function(e,g){var h=c.xhr(),i,j;c.username?h.open(c.type,c.url,c.async,c.username,c.password):h.open(c.type,c.url,c.async);if(c.xhrFields)for(j in c.xhrFields)h[j]=c.xhrFields[j];c.mimeType&&h.overrideMimeType&&h.overrideMimeType(c.mimeType),!c.crossDomain&&!e["X-Requested-With"]&&(e["X-Requested-With"]="XMLHttpRequest");try{for(j in e)h.setRequestHeader(j,e[j])}catch(k){}h.send(c.hasContent&&c.data||null),d=function(a,e){var j,k,l,m,n;try{if(d&&(e||h.readyState===4)){d=b,i&&(h.onreadystatechange=f.noop,cf&&delete ch[i]);if(e)h.readyState!==4&&h.abort();else{j=h.status,l=h.getAllResponseHeaders(),m={},n=h.responseXML,n&&n.documentElement&&(m.xml=n),m.text=h.responseText;try{k=h.statusText}catch(o){k=""}!j&&c.isLocal&&!c.crossDomain?j=m.text?200:404:j===1223&&(j=204)}}}catch(p){e||g(-1,p)}m&&g(j,k,m,l)},!c.async||h.readyState===4?d():(i=++cg,cf&&(ch||(ch={},f(a).unload(cf)),ch[i]=d),h.onreadystatechange=d)},abort:function(){d&&d(0,1)}}}});var ck={},cl,cm,cn=/^(?:toggle|show|hide)$/,co=/^([+\-]=)?([\d+.\-]+)([a-z%]*)$/i,cp,cq=[["height","marginTop","marginBottom","paddingTop","paddingBottom"],["width","marginLeft","marginRight","paddingLeft","paddingRight"],["opacity"]],cr;f.fn.extend({show:function(a,b,c){var d,e;if(a||a===0)return this.animate(cu("show",3),a,b,c);for(var g=0,h=this.length;g=i.duration+this.startTime){this.now=this.end,this.pos=this.state=1,this.update(),i.animatedProperties[this.prop]=!0;for(b in i.animatedProperties)i.animatedProperties[b]!==!0&&(g=!1);if(g){i.overflow!=null&&!f.support.shrinkWrapBlocks&&f.each(["","X","Y"],function(a,b){h.style["overflow"+b]=i.overflow[a]}),i.hide&&f(h).hide();if(i.hide||i.show)for(b in i.animatedProperties)f.style(h,b,i.orig[b]),f.removeData(h,"fxshow"+b,!0),f.removeData(h,"toggle"+b,!0);d=i.complete,d&&(i.complete=!1,d.call(h))}return!1}i.duration==Infinity?this.now=e:(c=e-this.startTime,this.state=c/i.duration,this.pos=f.easing[i.animatedProperties[this.prop]](this.state,c,0,1,i.duration),this.now=this.start+(this.end-this.start)*this.pos),this.update();return!0}},f.extend(f.fx,{tick:function(){var a,b=f.timers,c=0;for(;c-1,k={},l={},m,n;j?(l=e.position(),m=l.top,n=l.left):(m=parseFloat(h)||0,n=parseFloat(i)||0),f.isFunction(b)&&(b=b.call(a,c,g)),b.top!=null&&(k.top=b.top-g.top+m),b.left!=null&&(k.left=b.left-g.left+n),"using"in b?b.using.call(a,k):e.css(k)}},f.fn.extend({position:function(){if(!this[0])return null;var a=this[0],b=this.offsetParent(),c=this.offset(),d=cx.test(b[0].nodeName)?{top:0,left:0}:b.offset();c.top-=parseFloat(f.css(a,"marginTop"))||0,c.left-=parseFloat(f.css(a,"marginLeft"))||0,d.top+=parseFloat(f.css(b[0],"borderTopWidth"))||0,d.left+=parseFloat(f.css(b[0],"borderLeftWidth"))||0;return{top:c.top-d.top,left:c.left-d.left}},offsetParent:function(){return this.map(function(){var a=this.offsetParent||c.body;while(a&&!cx.test(a.nodeName)&&f.css(a,"position")==="static")a=a.offsetParent;return a})}}),f.each(["Left","Top"],function(a,c){var d="scroll"+c;f.fn[d]=function(c){var e,g;if(c===b){e=this[0];if(!e)return null;g=cy(e);return g?"pageXOffset"in g?g[a?"pageYOffset":"pageXOffset"]:f.support.boxModel&&g.document.documentElement[d]||g.document.body[d]:e[d]}return this.each(function(){g=cy(this),g?g.scrollTo(a?f(g).scrollLeft():c,a?c:f(g).scrollTop()):this[d]=c})}}),f.each(["Height","Width"],function(a,c){var d=c.toLowerCase();f.fn["inner"+c]=function(){var a=this[0];return a?a.style?parseFloat(f.css(a,d,"padding")):this[d]():null},f.fn["outer"+c]=function(a){var b=this[0];return b?b.style?parseFloat(f.css(b,d,a?"margin":"border")):this[d]():null},f.fn[d]=function(a){var e=this[0];if(!e)return a==null?null:this;if(f.isFunction(a))return this.each(function(b){var c=f(this);c[d](a.call(this,b,c[d]()))});if(f.isWindow(e)){var g=e.document.documentElement["client"+c],h=e.document.body;return e.document.compatMode==="CSS1Compat"&&g||h&&h["client"+c]||g}if(e.nodeType===9)return Math.max(e.documentElement["client"+c],e.body["scroll"+c],e.documentElement["scroll"+c],e.body["offset"+c],e.documentElement["offset"+c]);if(a===b){var i=f.css(e,d),j=parseFloat(i);return f.isNumeric(j)?j:i}return this.css(d,typeof a=="string"?a:a+"px")}}),a.jQuery=a.$=f,typeof define=="function"&&define.amd&&define.amd.jQuery&&define("jquery",[],function(){return f})})(window); \ No newline at end of file diff --git a/docs/source/_static/api-reference/method_list.html b/docs/source/_static/api-reference/method_list.html new file mode 100644 index 00000000..d17f26aa --- /dev/null +++ b/docs/source/_static/api-reference/method_list.html @@ -0,0 +1,715 @@ + + + + + + + + + + + + + + + + + + Method List + + + +
+
+

Method List

+ + + +
+ + +
+ + diff --git a/docs/source/_static/api-reference/top-level-namespace.html b/docs/source/_static/api-reference/top-level-namespace.html new file mode 100644 index 00000000..285cd5df --- /dev/null +++ b/docs/source/_static/api-reference/top-level-namespace.html @@ -0,0 +1,126 @@ + + + + + + + Top Level Namespace + + — Documentation by YARD 0.9.34 + + + + + + + + + + + + + + + + + + + +
+ + +

Top Level Namespace + + + +

+
+ + + + + + +
+
Includes:
+
MultiTenantFindBy
+
+ + + + + + +
+ +

Defined Under Namespace

+

+ + + Modules: ActiveRecord, MultiTenant, MultiTenantFindBy, Sidekiq + + + + +

+ + + + + + + + + + + + + + +

Method Summary

+ +

Methods included from MultiTenantFindBy

+

#cached_find_by_statement

+ + +
+ + + +
+ + \ No newline at end of file diff --git a/docs/source/_templates/.gitignore b/docs/source/_templates/.gitignore new file mode 100644 index 00000000..5e7d2734 --- /dev/null +++ b/docs/source/_templates/.gitignore @@ -0,0 +1,4 @@ +# Ignore everything in this directory +* +# Except this file +!.gitignore diff --git a/docs/source/api-reference.rst b/docs/source/api-reference.rst new file mode 100644 index 00000000..917ed131 --- /dev/null +++ b/docs/source/api-reference.rst @@ -0,0 +1,8 @@ +.. _api-reference: + +API Reference +============= + +This section provides a detailed overview of the available classes, modules, and methods in the ``activerecord-multi-tenant`` gem. + +`Click here to open the HTML file <_static/api-reference/index.html>`_ \ No newline at end of file diff --git a/docs/source/appendix.rst b/docs/source/appendix.rst new file mode 100644 index 00000000..4a538d3a --- /dev/null +++ b/docs/source/appendix.rst @@ -0,0 +1,26 @@ +.. _appendix: + +Appendix +======== + +This section provides additional resources and acknowledgments related to ``activerecord-multi-tenant``. + +Glossary of Terms and Abbreviations +----------------------------------- + +- **Multi-tenancy:** A software architecture in which a single instance of software serves multiple tenants. +- **Tenant:** A group of users who share a common access with specific privileges to the software instance. +- **Tenant ID:** A unique identifier for a tenant. + + +References to External Resources +-------------------------------- + +- `Official Rails Guide `_: A comprehensive guide to Ruby on Rails. +- `ActiveRecord Documentation `_: Detailed documentation for ActiveRecord, the database toolkit used by ``activerecord-multi-tenant``. + +Acknowledgments and Credits +--------------------------- + +We would like to thank the Ruby on Rails community for their contributions to open source, which have made projects like ``activerecord-multi-tenant`` possible. +This gem was initially based on `acts_as_tenant `, and still shares some code. We thank the authors for their efforts. diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst new file mode 100644 index 00000000..3f3f3945 --- /dev/null +++ b/docs/source/changelog.rst @@ -0,0 +1,8 @@ +.. _changelog: + +Changelog +========= + +This section provides a history of changes for each version of ``activerecord-multi-tenant``. +For a complete history of changes, please refer to the official changelog on `GitHub `_. + diff --git a/docs/source/community-and-support.rst b/docs/source/community-and-support.rst new file mode 100644 index 00000000..a28d42e1 --- /dev/null +++ b/docs/source/community-and-support.rst @@ -0,0 +1,26 @@ +.. _community-and-support: + +Community and Support +===================== + +If you need help with ``activerecord-multi-tenant``, there are several resources available to you. + +GitHub Repository +----------------- + +The `activerecord-multi-tenant GitHub repository `_ is the first place to look for code, issues, and pull requests. + +Issue Tracker +------------- + +If you encounter issues with ``activerecord-multi-tenant``, you can report them in the `issue tracker `_. Please provide as much detail as possible so we can address the issue effectively. + +Discussion Forums +----------------- + +For general discussions about ``activerecord-multi-tenant``, you can use the `discussion forums `_. This is a great place to ask questions, share ideas, and engage with the ``activerecord-multi-tenant`` community. + +Documentation Feedback +---------------------- + +We strive to provide high-quality documentation for ``activerecord-multi-tenant``. If you have feedback or suggestions for improving the documentation, please open an issue in the `issue tracker `_. diff --git a/docs/source/conf.py b/docs/source/conf.py new file mode 100644 index 00000000..5d01cfb0 --- /dev/null +++ b/docs/source/conf.py @@ -0,0 +1,30 @@ +from datetime import date + +# Configuration file for the Sphinx documentation builder. +# +# For the full list of built-in configuration values, see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +# -- Project information ----------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information + +project = "ActiveRecord Multi-tenant" +copyright = f"{date.today().year} .Citus Data Licensed under the MIT license, see License for details. " +author = "Citus Data" +release = "2.2.0" + +# -- General configuration --------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration + +extensions = ["sphinxnotes.strike"] + +templates_path = ["_templates"] +exclude_patterns = [] + +language = "python" + +# -- Options for HTML output ------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output + +html_theme = "sphinx_rtd_theme" +html_static_path = ["_static"] diff --git a/docs/source/contributing.rst b/docs/source/contributing.rst new file mode 100644 index 00000000..a50a6608 --- /dev/null +++ b/docs/source/contributing.rst @@ -0,0 +1,70 @@ +.. _contributing: + +Contributing +============ + +We welcome contributions to ``activerecord-multi-tenant``! This section provides guidelines for contributing to the project. + +Overview of the Development Process +----------------------------------- + +``activerecord-multi-tenant`` is developed using a standard fork and pull request model. The `activerecord-multi-tenant GitHub repository `_ is the starting point for code contributions. + +Guidelines for Contributing +--------------------------- + +1. **Fork the Repository:** Start by forking the official ``activerecord-multi-tenant`` repository to your own GitHub account. + +2. **Clone the Repository:** Clone the forked repository to your local machine and add the official repository as an upstream remote: + + .. code-block:: bash + + $ git clone https://github.com/citusdata/activerecord-multi-tenant.git + $ cd activerecord-multi-tenant + $ git remote add upstream https://github.com/your-github-account/activerecord-multi-tenant.git + +3. **Create a Feature Branch:** Create a new branch for each feature or bugfix: + + .. code-block:: bash + + $ git checkout -b my-feature-branch + +4. **Commit Your Changes:** Make your changes and commit them to your feature branch. + +5. **Push to GitHub:** Push your changes to your fork on GitHub: + + .. code-block:: bash + + $ git push origin my-feature-branch + +6. **Submit a Pull Request:** Open a pull request from your feature branch to the master branch of the official ``activerecord-multi-tenant`` repository. + +Please ensure your code adheres to the existing style conventions of the project. Include tests for any new features or bug fixes, and update the documentation as necessary. + +Setting Up a Development Environment +------------------------------------ + +To set up a development environment for ``activerecord-multi-tenant``, follow these steps: + +1. Clone the repository as described in the contributing guidelines. + +2. Install the required dependencies: + + .. code-block:: bash + + $ bundle install + +3. Run the tests to ensure everything is set up correctly: + + .. code-block:: bash + + $ bundle exec rake spec + +4. Compile documentation for the project: + + .. code-block:: bash + + $ cd docs + $ make pre-build + $ make html + diff --git a/docs/source/getting-started.rst b/docs/source/getting-started.rst new file mode 100644 index 00000000..f6aeb123 --- /dev/null +++ b/docs/source/getting-started.rst @@ -0,0 +1,37 @@ +.. _getting-started: + +Getting Started +=============== + +This section will guide you through the process of installing and setting up ``activerecord-multi-tenant`` in your Rails application. + +Installation +------------ + +To install ``activerecord-multi-tenant``, add the following line to your application's Gemfile: + +.. code-block:: ruby + + gem install activerecord-multi-tenant + +Then execute: + +.. code-block:: bash + + $ bundle install + +Or install it yourself as: + +.. code-block:: bash + + $ gem install activerecord-multi-tenant + +Dependencies +------------ + +``activerecord-multi-tenant`` requires: + +- Ruby version 3.0.0 or later +- Rails version 6.0.0 or later + +Please ensure that your application meets these requirements before installing the gem. diff --git a/docs/source/guides-and-tutorials.rst b/docs/source/guides-and-tutorials.rst new file mode 100644 index 00000000..74ba29ce --- /dev/null +++ b/docs/source/guides-and-tutorials.rst @@ -0,0 +1,129 @@ +.. _guides-and-tutorials: + +Guides and Tutorials +==================== + +This section provides step-by-step guides and tutorials on how to use ``activerecord-multi-tenant`` in various scenarios. + +Setting Up Multi tenancy in a New Project +------------------------------------------ + +To set up multi-tenancy in a new Rails project, follow these steps: + +1. Install the ``activerecord-multi-tenant`` gem as described in the :ref:`getting-started` section. + +2. Declare your tenant model in your ActiveRecord models: + + .. code-block:: ruby + + class User < ActiveRecord::Base + multi_tenant :company + end + +3. Set the current tenant before executing queries: + + .. code-block:: ruby + + ActiveRecord::MultiTenant.current_tenant = Company.first + @users = User.all + +Migrating an Existing Project to Use ``activerecord-multi-tenant`` +------------------------------------------------------------------ + +If you have an existing Rails project and you want to add multi-tenancy support, follow these steps: + +1. Install the ``activerecord-multi-tenant`` gem as described in the :ref:`getting-started` section. + +2. Update your ActiveRecord models to declare the tenant model: + + .. code-block:: ruby + + class User < ActiveRecord::Base + multi_tenant :company + end + +3. Update your application logic to set the current tenant before executing queries. + + +Using ``has_many`` , ``has_one`` , and ``belongs_to`` Associations +------------------------------------------------------------------ + +When using ``has_many``, ``has_one``, and ``belongs_to`` associations, +there is nothing special you need to do to make them work with ``activerecord-multi-tenant``. +The gem will automatically scope the associations to the current tenant.: + +.. code-block:: ruby + + class User < ActiveRecord::Base + multi_tenant :company + has_many :posts + end + + class Post < ActiveRecord::Base + belongs_to :user + end + + ActiveRecord::MultiTenant.with(Company.first) do + @user = User.first + @user.posts # => Returns posts belonging to Company.first + end + +Using ``has_and_belongs_to_many`` Associations +----------------------------------------------- + +When using ``has_and_belongs_to_many`` associations, you need to specify the tenant column and tenant class name to +scope the association to the current tenant. If you set the ``tenant_enabled`` option to ``false``, the gem will +not scope the association to the current tenant. + +.. code-block:: ruby + + class Account < ActiveRecord::Base + multi_tenant :account + has_many :projects + has_one :manager, inverse_of: :account + has_many :optional_sub_tasks + end + + class Manager < ActiveRecord::Base + multi_tenant :account + belongs_to :project + has_and_belongs_to_many :tasks, { tenant_column: :account_id, tenant_enabled: true, + tenant_class_name: 'Account' } + end + + # Tests to check if the tenant column is set correctly + let(:task1) { Task.create! name: 'task1', project: project1, account: account1 } + let(:manager1) { Manager.create! name: 'manager1', account: account1, tasks: [task1] } + + MultiTenant.with(account1) do + expect(manager1.tasks.first.account_id).to eq(task1.account_id) # true + end + +Using ``activerecord-multi-tenant`` with Controllers +----------------------------------------------------- + +When using ``activerecord-multi-tenant`` with controllers, you need to set the current tenant in the controller +before executing queries. You can do this by overriding the ``set_current_tenant`` method in your controller: + +.. code-block:: ruby + + class ApplicationController < ActionController::Base + set_current_tenant_through_filter # Required to opt into this behavior + before_action :set_customer_as_tenant + + def set_customer_as_tenant + customer = Customer.find(session[:current_customer_id]) + set_current_tenant(customer) + end + end + +Best Practices and Recommendations +----------------------------------- + +When using ``activerecord-multi-tenant``, keep the following best practices in mind: + +- Always set the current tenant before executing queries in a multitenant context. +- Be mindful of the tenant scope when writing complex queries or joins. +- If you prefer not to set a tenant for the global context, but need to specify one for certain sections of code, + you can utilize the `MultiTenant.with(tenant)` function. This will assign the `tenant` value + to the specific code block where it's used. diff --git a/docs/source/index.rst b/docs/source/index.rst new file mode 100644 index 00000000..6e5c26aa --- /dev/null +++ b/docs/source/index.rst @@ -0,0 +1,54 @@ +.. Django Multi-tenant documentation master file, created by + sphinx-quickstart on Mon Feb 13 13:32:28 2023. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Welcome to ActiveRecord Multi-tenant's documentation! +====================================================== + +|Build Status| |Coverage Status| |RubyGems Version| |Gem Downloads| |Latest Documentation Status| + +.. |Build Status| image:: https://github.com/citusdata/activerecord-multi-tenant/actions/workflows/active-record-multi-tenant-tests.yml/badge.svg + :target: https://github.com/citusdata/activerecord-multi-tenant/actions/workflows/active-record-multi-tenant-tests.yml + :alt: Build Status + +.. |Coverage Status| image:: https://codecov.io/gh/citusdata/activerecord-multi-tenant/branch/master/graph/badge.svg?token=rw0TsEk4Ld + :target: https://codecov.io/gh/citusdata/activerecord-multi-tenant + :alt: Coverage Status + +.. |RubyGems Version| image:: https://img.shields.io/gem/v/activerecord-multi-tenant.svg + :target: https://rubygems.org/gems/activerecord-multi-tenant + +.. |Gem Downloads| image:: https://img.shields.io/gem/dt/activerecord-multi-tenant.svg + :target: https://rubygems.org/gems/activerecord-multi-tenant + :alt: Gem Downloads + +.. |Latest Documentation Status| image:: https://readthedocs.org/projects/django-multitenant/badge/?version=latest + :target: https://activerecord-multi-tenant.readthedocs.io/en/latest/?badge=latest + :alt: Documentation Status + + +activerecord-multi-tenant Documentation +======================================== + +Welcome to the official documentation for ``activerecord-multi-tenant``, a powerful and flexible gem for adding multi-tenancy support to your Rails applications using ActiveRecord. + +.. toctree:: + :maxdepth: 2 + :caption: Table of Contents + + introduction + getting-started + usage-guide + guides-and-tutorials + api-reference + troubleshooting + contributing + changelog + community-and-support + appendix + license + +This documentation provides a comprehensive guide to using ``activerecord-multi-tenant``, including installation and configuration instructions, usage examples, API reference, troubleshooting tips, and more. + +Whether you're new to multi-tenancy or an experienced developer looking to integrate ``activerecord-multi-tenant`` into your existing Rails application, this documentation aims to provide the information you need. diff --git a/docs/source/introduction.rst b/docs/source/introduction.rst new file mode 100644 index 00000000..e76d15e3 --- /dev/null +++ b/docs/source/introduction.rst @@ -0,0 +1,33 @@ +.. _introduction: + +Introduction +============ + +Welcome to the official documentation for ``activerecord-multi-tenant``, a powerful and flexible gem for adding multi-tenancy support to your Rails applications using ActiveRecord. + +Overview +-------- + +``activerecord-multi-tenant`` is designed to help developers manage and interact with data in multi-tenant environments. It provides a simple and intuitive API to scope your ActiveRecord models to specific tenants, ensuring data isolation and security while maintaining the simplicity and elegance that Rails developers love. + +Purpose +------- + +The purpose of this documentation is to provide a comprehensive guide to using ``activerecord-multi-tenant``. Whether you're new to multi-tenancy or an experienced developer looking to integrate ``activerecord-multi-tenant`` into your existing Rails application, this documentation aims to provide the information you need. + +Benefits of Using ``activerecord-multi-tenant`` +------------------------------------------------ + +With ``activerecord-multi-tenant``, you can: + +- Easily scope your ActiveRecord models to specific tenants +- Maintain data isolation and security in multi-tenant environments +- Seamlessly integrate with your existing Rails applications +- Benefit from the simplicity and elegance of the Rails framework + +Target Audience +--------------- + +This documentation is intended for developers who are familiar with Ruby on Rails and ActiveRecord. Knowledge of multi-tenancy concepts is beneficial but not required, as we will cover these topics in the following sections. + +We hope you find this documentation helpful as you explore the capabilities of ``activerecord-multi-tenant``. Let's get started! diff --git a/docs/source/license.rst b/docs/source/license.rst new file mode 100644 index 00000000..3258400a --- /dev/null +++ b/docs/source/license.rst @@ -0,0 +1,22 @@ +.. _license: + +License +=============================================== +Copyright (c) 2023 , Citus Data Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the +Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/docs/source/troubleshooting.rst b/docs/source/troubleshooting.rst new file mode 100644 index 00000000..3fb791dc --- /dev/null +++ b/docs/source/troubleshooting.rst @@ -0,0 +1,41 @@ +.. _troubleshooting: + +Troubleshooting +=============== + +This section provides solutions to common issues you might encounter when using ``activerecord-multi-tenant``. + +Common Issues and Their Solutions +--------------------------------- + +**Issue:** Tenant scope is not applied to queries. + +**Solution:** Make sure you've set the current tenant before executing queries. Use the MultiTenant.with method to set the current tenant. For example: + +.. code-block:: ruby + + MultiTenant.with(customer) do + site = Site.find(params[:site_id]) + site.update! last_accessed_at: Time.now + site.page_views.count + end + + + +FAQs and Known Limitations +-------------------------- + +**Q: Can I use multiple tenant models in the same application?** + +**A:** Yes, you can declare different tenant models in different ActiveRecord models. However, you can only set one current tenant at a time. + +**Q: Does ``activerecord-multi-tenant`` support Rails version 5.X?** + +**A:** ``activerecord-multi-tenant`` supports Rails 6.0.0 and later. For older versions of Rails, please use the appropriate version of the gem. + + + +Reporting Bugs and Requesting Features +-------------------------------------- + +If you encounter a bug or have a feature request, please open an issue on the `activerecord-multi-tenant GitHub repository `_. Please provide as much detail as possible so we can address the issue effectively. diff --git a/docs/source/usage-guide.rst b/docs/source/usage-guide.rst new file mode 100644 index 00000000..7025a73f --- /dev/null +++ b/docs/source/usage-guide.rst @@ -0,0 +1,59 @@ +.. _usage-guide: + +Usage Guide +=========== + +This section provides a comprehensive guide on how to use ``activerecord-multi-tenant`` in your Rails application. + +Basic Usage +----------- + +To use ``activerecord-multi-tenant``, you need to declare the tenant model in your ActiveRecord models. Here's an example: + +.. code-block:: ruby + + class PageView < ActiveRecord::Base + multi_tenant :customer + belongs_to :site + + # ... + end + + class Site < ActiveRecord::Base + multi_tenant :customer + has_many :page_views + + # ... + end + +In this example, the ``PageView`` and ``Site`` models are scoped to the ``Customer`` model. This means that each user belongs to a specific customer. + + +Then wrap all code that runs queries/modifications in blocks like this: + +.. code-block:: ruby + + customer = Customer.find(session[:current_customer_id]) + # ... + MultiTenant.with(customer) do + site = Site.find(params[:site_id]) + site.update! last_accessed_at: Time.now + site.page_views.count + end + +Alternatively, if you don't want to use a block, you can set the current tenant explicitly: + +.. code-block:: ruby + + customer = Customer.find(session[:current_customer_id]) + MultiTenant.current_tenant = customer + + +Multi-tenancy Concepts and Terminology +-------------------------------------- + +Multi-tenancy is a software architecture in which a single instance of software serves multiple tenants. A tenant is a group of users who share a common access with specific privileges to the software instance. + +In the context of ``activerecord-multi-tenant``, a tenant is typically represented by a model in your Rails application (e.g., ``Customer``), and other models (e.g., ``PageView``) are scoped to this tenant model. + + diff --git a/gemfiles/.bundle/config b/gemfiles/.bundle/config deleted file mode 100644 index c127f802..00000000 --- a/gemfiles/.bundle/config +++ /dev/null @@ -1,2 +0,0 @@ ---- -BUNDLE_RETRY: "1" diff --git a/gemfiles/active_record_5.2.gemfile b/gemfiles/active_record_5.2.gemfile deleted file mode 100644 index 40886171..00000000 --- a/gemfiles/active_record_5.2.gemfile +++ /dev/null @@ -1,16 +0,0 @@ -# This file was generated by Appraisal - -source "https://rubygems.org" - -gem "appraisal" -gem "activerecord", "~> 5.2.0" -gem "i18n", "~> 0.9.5" -gem "nokogiri", "~> 1.7.1" -gem "nio4r", "~> 2.3.1" -gem "sprockets", "~> 3.7.1" -gem "byebug", "~> 11.0" -gem "rake", "12.0.0" -gem "redis", "3.3.3" -gem "pry-byebug", "3.9.0" - -gemspec path: "../" diff --git a/gemfiles/active_record_5.2.gemfile.lock b/gemfiles/active_record_5.2.gemfile.lock deleted file mode 100644 index 6e9e049b..00000000 --- a/gemfiles/active_record_5.2.gemfile.lock +++ /dev/null @@ -1,188 +0,0 @@ -PATH - remote: .. - specs: - activerecord-multi-tenant (1.1.1) - rails (>= 4.2) - request_store (>= 1.0.5) - -GEM - remote: https://rubygems.org/ - specs: - actioncable (5.2.3) - actionpack (= 5.2.3) - nio4r (~> 2.0) - websocket-driver (>= 0.6.1) - actionmailer (5.2.3) - actionpack (= 5.2.3) - actionview (= 5.2.3) - activejob (= 5.2.3) - mail (~> 2.5, >= 2.5.4) - rails-dom-testing (~> 2.0) - actionpack (5.2.3) - actionview (= 5.2.3) - activesupport (= 5.2.3) - rack (~> 2.0) - rack-test (>= 0.6.3) - rails-dom-testing (~> 2.0) - rails-html-sanitizer (~> 1.0, >= 1.0.2) - actionview (5.2.3) - activesupport (= 5.2.3) - builder (~> 3.1) - erubi (~> 1.4) - rails-dom-testing (~> 2.0) - rails-html-sanitizer (~> 1.0, >= 1.0.3) - activejob (5.2.3) - activesupport (= 5.2.3) - globalid (>= 0.3.6) - activemodel (5.2.3) - activesupport (= 5.2.3) - activerecord (5.2.3) - activemodel (= 5.2.3) - activesupport (= 5.2.3) - arel (>= 9.0) - activestorage (5.2.3) - actionpack (= 5.2.3) - activerecord (= 5.2.3) - marcel (~> 0.3.1) - activesupport (5.2.3) - concurrent-ruby (~> 1.0, >= 1.0.2) - i18n (>= 0.7, < 2) - minitest (~> 5.1) - tzinfo (~> 1.1) - appraisal (2.2.0) - bundler - rake - thor (>= 0.14.0) - arel (9.0.0) - builder (3.2.4) - byebug (11.1.3) - coderay (1.1.2) - concurrent-ruby (1.1.6) - connection_pool (2.2.2) - crass (1.0.6) - diff-lcs (1.3) - erubi (1.9.0) - globalid (0.4.2) - activesupport (>= 4.2.0) - i18n (0.9.5) - concurrent-ruby (~> 1.0) - loofah (2.5.0) - crass (~> 1.0.2) - nokogiri (>= 1.5.9) - mail (2.7.1) - mini_mime (>= 0.1.1) - marcel (0.3.3) - mimemagic (~> 0.3.2) - method_source (1.0.0) - mimemagic (0.3.5) - mini_mime (1.0.2) - mini_portile2 (2.1.0) - minitest (5.14.1) - nio4r (2.3.1) - nokogiri (1.7.2) - mini_portile2 (~> 2.1.0) - pg (1.2.3) - pry (0.13.1) - coderay (~> 1.1) - method_source (~> 1.0) - pry-byebug (3.9.0) - byebug (~> 11.0) - pry (~> 0.13.0) - rack (2.2.2) - rack-protection (2.0.8.1) - rack - rack-test (1.1.0) - rack (>= 1.0, < 3) - rails (5.2.3) - actioncable (= 5.2.3) - actionmailer (= 5.2.3) - actionpack (= 5.2.3) - actionview (= 5.2.3) - activejob (= 5.2.3) - activemodel (= 5.2.3) - activerecord (= 5.2.3) - activestorage (= 5.2.3) - activesupport (= 5.2.3) - bundler (>= 1.3.0) - railties (= 5.2.3) - sprockets-rails (>= 2.0.0) - rails-dom-testing (2.0.3) - activesupport (>= 4.2.0) - nokogiri (>= 1.6) - rails-html-sanitizer (1.3.0) - loofah (~> 2.3) - railties (5.2.3) - actionpack (= 5.2.3) - activesupport (= 5.2.3) - method_source - rake (>= 0.8.7) - thor (>= 0.19.0, < 2.0) - rake (12.0.0) - redis (3.3.3) - request_store (1.5.0) - rack (>= 1.4) - rspec (3.9.0) - rspec-core (~> 3.9.0) - rspec-expectations (~> 3.9.0) - rspec-mocks (~> 3.9.0) - rspec-core (3.9.2) - rspec-support (~> 3.9.3) - rspec-expectations (3.9.2) - diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.9.0) - rspec-mocks (3.9.1) - diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.9.0) - rspec-rails (4.0.1) - actionpack (>= 4.2) - activesupport (>= 4.2) - railties (>= 4.2) - rspec-core (~> 3.9) - rspec-expectations (~> 3.9) - rspec-mocks (~> 3.9) - rspec-support (~> 3.9) - rspec-support (3.9.3) - sidekiq (5.0.4) - concurrent-ruby (~> 1.0) - connection_pool (~> 2.2, >= 2.2.0) - rack-protection (>= 1.5.0) - redis (~> 3.3, >= 3.3.3) - sprockets (3.7.2) - concurrent-ruby (~> 1.0) - rack (> 1, < 3) - sprockets-rails (3.2.2) - actionpack (>= 4.0) - activesupport (>= 4.0) - sprockets (>= 3.0.0) - thor (1.0.1) - thread_safe (0.3.6) - tzinfo (1.2.7) - thread_safe (~> 0.1) - websocket-driver (0.7.3) - websocket-extensions (>= 0.1.0) - websocket-extensions (0.1.5) - -PLATFORMS - ruby - -DEPENDENCIES - activerecord (~> 5.2.0) - activerecord-multi-tenant! - appraisal - byebug (~> 11.0) - i18n (~> 0.9.5) - nio4r (~> 2.3.1) - nokogiri (~> 1.7.1) - pg - pry - pry-byebug (= 3.9.0) - rake (= 12.0.0) - redis (= 3.3.3) - rspec (>= 3.0) - rspec-rails - sidekiq - sprockets (~> 3.7.1) - thor - -BUNDLED WITH - 2.1.4 diff --git a/gemfiles/active_record_6.0.gemfile b/gemfiles/active_record_6.0.gemfile deleted file mode 100644 index 34c84998..00000000 --- a/gemfiles/active_record_6.0.gemfile +++ /dev/null @@ -1,8 +0,0 @@ -# This file was generated by Appraisal - -source "https://rubygems.org" - -gem "appraisal" -gem "activerecord", "~> 6.0.3" - -gemspec path: "../" diff --git a/gemfiles/active_record_6.0.gemfile.lock b/gemfiles/active_record_6.0.gemfile.lock deleted file mode 100644 index 34242ca8..00000000 --- a/gemfiles/active_record_6.0.gemfile.lock +++ /dev/null @@ -1,198 +0,0 @@ -PATH - remote: .. - specs: - activerecord-multi-tenant (1.1.1) - rails (>= 4.2) - request_store (>= 1.0.5) - -GEM - remote: https://rubygems.org/ - specs: - actioncable (6.0.3.1) - actionpack (= 6.0.3.1) - nio4r (~> 2.0) - websocket-driver (>= 0.6.1) - actionmailbox (6.0.3.1) - actionpack (= 6.0.3.1) - activejob (= 6.0.3.1) - activerecord (= 6.0.3.1) - activestorage (= 6.0.3.1) - activesupport (= 6.0.3.1) - mail (>= 2.7.1) - actionmailer (6.0.3.1) - actionpack (= 6.0.3.1) - actionview (= 6.0.3.1) - activejob (= 6.0.3.1) - mail (~> 2.5, >= 2.5.4) - rails-dom-testing (~> 2.0) - actionpack (6.0.3.1) - actionview (= 6.0.3.1) - activesupport (= 6.0.3.1) - rack (~> 2.0, >= 2.0.8) - rack-test (>= 0.6.3) - rails-dom-testing (~> 2.0) - rails-html-sanitizer (~> 1.0, >= 1.2.0) - actiontext (6.0.3.1) - actionpack (= 6.0.3.1) - activerecord (= 6.0.3.1) - activestorage (= 6.0.3.1) - activesupport (= 6.0.3.1) - nokogiri (>= 1.8.5) - actionview (6.0.3.1) - activesupport (= 6.0.3.1) - builder (~> 3.1) - erubi (~> 1.4) - rails-dom-testing (~> 2.0) - rails-html-sanitizer (~> 1.1, >= 1.2.0) - activejob (6.0.3.1) - activesupport (= 6.0.3.1) - globalid (>= 0.3.6) - activemodel (6.0.3.1) - activesupport (= 6.0.3.1) - activerecord (6.0.3.1) - activemodel (= 6.0.3.1) - activesupport (= 6.0.3.1) - activestorage (6.0.3.1) - actionpack (= 6.0.3.1) - activejob (= 6.0.3.1) - activerecord (= 6.0.3.1) - marcel (~> 0.3.1) - activesupport (6.0.3.1) - concurrent-ruby (~> 1.0, >= 1.0.2) - i18n (>= 0.7, < 2) - minitest (~> 5.1) - tzinfo (~> 1.1) - zeitwerk (~> 2.2, >= 2.2.2) - appraisal (2.2.0) - bundler - rake - thor (>= 0.14.0) - builder (3.2.4) - byebug (11.1.3) - coderay (1.1.2) - concurrent-ruby (1.1.6) - connection_pool (2.2.2) - crass (1.0.6) - diff-lcs (1.3) - erubi (1.9.0) - globalid (0.4.2) - activesupport (>= 4.2.0) - i18n (1.8.2) - concurrent-ruby (~> 1.0) - loofah (2.5.0) - crass (~> 1.0.2) - nokogiri (>= 1.5.9) - mail (2.7.1) - mini_mime (>= 0.1.1) - marcel (0.3.3) - mimemagic (~> 0.3.2) - method_source (1.0.0) - mimemagic (0.3.5) - mini_mime (1.0.2) - mini_portile2 (2.4.0) - minitest (5.14.1) - nio4r (2.5.4) - nokogiri (1.10.9) - mini_portile2 (~> 2.4.0) - pg (1.2.3) - pry (0.13.1) - coderay (~> 1.1) - method_source (~> 1.0) - pry-byebug (3.9.0) - byebug (~> 11.0) - pry (~> 0.13.0) - rack (2.2.2) - rack-protection (2.0.8.1) - rack - rack-test (1.1.0) - rack (>= 1.0, < 3) - rails (6.0.3.1) - actioncable (= 6.0.3.1) - actionmailbox (= 6.0.3.1) - actionmailer (= 6.0.3.1) - actionpack (= 6.0.3.1) - actiontext (= 6.0.3.1) - actionview (= 6.0.3.1) - activejob (= 6.0.3.1) - activemodel (= 6.0.3.1) - activerecord (= 6.0.3.1) - activestorage (= 6.0.3.1) - activesupport (= 6.0.3.1) - bundler (>= 1.3.0) - railties (= 6.0.3.1) - sprockets-rails (>= 2.0.0) - rails-dom-testing (2.0.3) - activesupport (>= 4.2.0) - nokogiri (>= 1.6) - rails-html-sanitizer (1.3.0) - loofah (~> 2.3) - railties (6.0.3.1) - actionpack (= 6.0.3.1) - activesupport (= 6.0.3.1) - method_source - rake (>= 0.8.7) - thor (>= 0.20.3, < 2.0) - rake (13.0.1) - redis (4.1.4) - request_store (1.5.0) - rack (>= 1.4) - rspec (3.9.0) - rspec-core (~> 3.9.0) - rspec-expectations (~> 3.9.0) - rspec-mocks (~> 3.9.0) - rspec-core (3.9.2) - rspec-support (~> 3.9.3) - rspec-expectations (3.9.2) - diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.9.0) - rspec-mocks (3.9.1) - diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.9.0) - rspec-rails (4.0.1) - actionpack (>= 4.2) - activesupport (>= 4.2) - railties (>= 4.2) - rspec-core (~> 3.9) - rspec-expectations (~> 3.9) - rspec-mocks (~> 3.9) - rspec-support (~> 3.9) - rspec-support (3.9.3) - sidekiq (6.0.7) - connection_pool (>= 2.2.2) - rack (~> 2.0) - rack-protection (>= 2.0.0) - redis (>= 4.1.0) - sprockets (4.0.2) - concurrent-ruby (~> 1.0) - rack (> 1, < 3) - sprockets-rails (3.2.2) - actionpack (>= 4.0) - activesupport (>= 4.0) - sprockets (>= 3.0.0) - thor (1.0.1) - thread_safe (0.3.6) - tzinfo (1.2.7) - thread_safe (~> 0.1) - websocket-driver (0.7.3) - websocket-extensions (>= 0.1.0) - websocket-extensions (0.1.5) - zeitwerk (2.3.0) - -PLATFORMS - ruby - -DEPENDENCIES - activerecord (~> 6.0.3) - activerecord-multi-tenant! - appraisal - pg - pry - pry-byebug - rake - rspec (>= 3.0) - rspec-rails - sidekiq - thor - -BUNDLED WITH - 2.1.4 diff --git a/gemfiles/active_record_6.1.gemfile b/gemfiles/active_record_6.1.gemfile deleted file mode 100644 index ea2599ce..00000000 --- a/gemfiles/active_record_6.1.gemfile +++ /dev/null @@ -1,8 +0,0 @@ -# This file was generated by Appraisal - -source "https://rubygems.org" - -gem "appraisal" -gem "activerecord", "~> 6.1.0" - -gemspec path: "../" diff --git a/gemfiles/active_record_6.1.gemfile.lock b/gemfiles/active_record_6.1.gemfile.lock deleted file mode 100644 index eb6b5c43..00000000 --- a/gemfiles/active_record_6.1.gemfile.lock +++ /dev/null @@ -1,198 +0,0 @@ -PATH - remote: .. - specs: - activerecord-multi-tenant (1.1.1) - rails (>= 4.2) - request_store (>= 1.0.5) - -GEM - remote: https://rubygems.org/ - specs: - actioncable (6.1.0) - actionpack (= 6.1.0) - activesupport (= 6.1.0) - nio4r (~> 2.0) - websocket-driver (>= 0.6.1) - actionmailbox (6.1.0) - actionpack (= 6.1.0) - activejob (= 6.1.0) - activerecord (= 6.1.0) - activestorage (= 6.1.0) - activesupport (= 6.1.0) - mail (>= 2.7.1) - actionmailer (6.1.0) - actionpack (= 6.1.0) - actionview (= 6.1.0) - activejob (= 6.1.0) - activesupport (= 6.1.0) - mail (~> 2.5, >= 2.5.4) - rails-dom-testing (~> 2.0) - actionpack (6.1.0) - actionview (= 6.1.0) - activesupport (= 6.1.0) - rack (~> 2.0, >= 2.0.9) - rack-test (>= 0.6.3) - rails-dom-testing (~> 2.0) - rails-html-sanitizer (~> 1.0, >= 1.2.0) - actiontext (6.1.0) - actionpack (= 6.1.0) - activerecord (= 6.1.0) - activestorage (= 6.1.0) - activesupport (= 6.1.0) - nokogiri (>= 1.8.5) - actionview (6.1.0) - activesupport (= 6.1.0) - builder (~> 3.1) - erubi (~> 1.4) - rails-dom-testing (~> 2.0) - rails-html-sanitizer (~> 1.1, >= 1.2.0) - activejob (6.1.0) - activesupport (= 6.1.0) - globalid (>= 0.3.6) - activemodel (6.1.0) - activesupport (= 6.1.0) - activerecord (6.1.0) - activemodel (= 6.1.0) - activesupport (= 6.1.0) - activestorage (6.1.0) - actionpack (= 6.1.0) - activejob (= 6.1.0) - activerecord (= 6.1.0) - activesupport (= 6.1.0) - marcel (~> 0.3.1) - mimemagic (~> 0.3.2) - activesupport (6.1.0) - concurrent-ruby (~> 1.0, >= 1.0.2) - i18n (>= 1.6, < 2) - minitest (>= 5.1) - tzinfo (~> 2.0) - zeitwerk (~> 2.3) - appraisal (2.3.0) - bundler - rake - thor (>= 0.14.0) - builder (3.2.4) - byebug (11.1.3) - coderay (1.1.3) - concurrent-ruby (1.1.7) - connection_pool (2.2.3) - crass (1.0.6) - diff-lcs (1.4.4) - erubi (1.10.0) - globalid (0.4.2) - activesupport (>= 4.2.0) - i18n (1.8.5) - concurrent-ruby (~> 1.0) - loofah (2.8.0) - crass (~> 1.0.2) - nokogiri (>= 1.5.9) - mail (2.7.1) - mini_mime (>= 0.1.1) - marcel (0.3.3) - mimemagic (~> 0.3.2) - method_source (1.0.0) - mimemagic (0.3.5) - mini_mime (1.0.2) - mini_portile2 (2.4.0) - minitest (5.14.2) - nio4r (2.5.4) - nokogiri (1.10.10) - mini_portile2 (~> 2.4.0) - pg (1.2.3) - pry (0.13.1) - coderay (~> 1.1) - method_source (~> 1.0) - pry-byebug (3.9.0) - byebug (~> 11.0) - pry (~> 0.13.0) - rack (2.2.3) - rack-test (1.1.0) - rack (>= 1.0, < 3) - rails (6.1.0) - actioncable (= 6.1.0) - actionmailbox (= 6.1.0) - actionmailer (= 6.1.0) - actionpack (= 6.1.0) - actiontext (= 6.1.0) - actionview (= 6.1.0) - activejob (= 6.1.0) - activemodel (= 6.1.0) - activerecord (= 6.1.0) - activestorage (= 6.1.0) - activesupport (= 6.1.0) - bundler (>= 1.15.0) - railties (= 6.1.0) - sprockets-rails (>= 2.0.0) - rails-dom-testing (2.0.3) - activesupport (>= 4.2.0) - nokogiri (>= 1.6) - rails-html-sanitizer (1.3.0) - loofah (~> 2.3) - railties (6.1.0) - actionpack (= 6.1.0) - activesupport (= 6.1.0) - method_source - rake (>= 0.8.7) - thor (~> 1.0) - rake (13.0.3) - redis (4.2.5) - request_store (1.5.0) - rack (>= 1.4) - rspec (3.10.0) - rspec-core (~> 3.10.0) - rspec-expectations (~> 3.10.0) - rspec-mocks (~> 3.10.0) - rspec-core (3.10.1) - rspec-support (~> 3.10.0) - rspec-expectations (3.10.1) - diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.10.0) - rspec-mocks (3.10.1) - diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.10.0) - rspec-rails (4.0.2) - actionpack (>= 4.2) - activesupport (>= 4.2) - railties (>= 4.2) - rspec-core (~> 3.10) - rspec-expectations (~> 3.10) - rspec-mocks (~> 3.10) - rspec-support (~> 3.10) - rspec-support (3.10.1) - sidekiq (6.1.2) - connection_pool (>= 2.2.2) - rack (~> 2.0) - redis (>= 4.2.0) - sprockets (4.0.2) - concurrent-ruby (~> 1.0) - rack (> 1, < 3) - sprockets-rails (3.2.2) - actionpack (>= 4.0) - activesupport (>= 4.0) - sprockets (>= 3.0.0) - thor (1.0.1) - tzinfo (2.0.4) - concurrent-ruby (~> 1.0) - websocket-driver (0.7.3) - websocket-extensions (>= 0.1.0) - websocket-extensions (0.1.5) - zeitwerk (2.4.2) - -PLATFORMS - ruby - -DEPENDENCIES - activerecord (~> 6.1.0) - activerecord-multi-tenant! - appraisal - pg - pry - pry-byebug - rake - rspec (>= 3.0) - rspec-rails - sidekiq - thor - -BUNDLED WITH - 2.1.4 diff --git a/gemfiles/rails_5.2.gemfile b/gemfiles/rails_5.2.gemfile deleted file mode 100644 index 3124d81a..00000000 --- a/gemfiles/rails_5.2.gemfile +++ /dev/null @@ -1,16 +0,0 @@ -# This file was generated by Appraisal - -source "https://rubygems.org" - -gem "appraisal" -gem "rails", "~> 5.2.0" -gem "i18n", "~> 0.9.5" -gem "nokogiri", "~> 1.7.1" -gem "nio4r", "~> 2.3.1" -gem "sprockets", "~> 3.7.1" -gem "byebug", "~> 11.0" -gem "rake", "12.0.0" -gem "redis", "3.3.3" -gem "pry-byebug", "3.9.0" - -gemspec path: "../" diff --git a/gemfiles/rails_5.2.gemfile.lock b/gemfiles/rails_5.2.gemfile.lock deleted file mode 100644 index ffcc57a4..00000000 --- a/gemfiles/rails_5.2.gemfile.lock +++ /dev/null @@ -1,188 +0,0 @@ -PATH - remote: .. - specs: - activerecord-multi-tenant (1.1.1) - rails (>= 4.2) - request_store (>= 1.0.5) - -GEM - remote: https://rubygems.org/ - specs: - actioncable (5.2.3) - actionpack (= 5.2.3) - nio4r (~> 2.0) - websocket-driver (>= 0.6.1) - actionmailer (5.2.3) - actionpack (= 5.2.3) - actionview (= 5.2.3) - activejob (= 5.2.3) - mail (~> 2.5, >= 2.5.4) - rails-dom-testing (~> 2.0) - actionpack (5.2.3) - actionview (= 5.2.3) - activesupport (= 5.2.3) - rack (~> 2.0) - rack-test (>= 0.6.3) - rails-dom-testing (~> 2.0) - rails-html-sanitizer (~> 1.0, >= 1.0.2) - actionview (5.2.3) - activesupport (= 5.2.3) - builder (~> 3.1) - erubi (~> 1.4) - rails-dom-testing (~> 2.0) - rails-html-sanitizer (~> 1.0, >= 1.0.3) - activejob (5.2.3) - activesupport (= 5.2.3) - globalid (>= 0.3.6) - activemodel (5.2.3) - activesupport (= 5.2.3) - activerecord (5.2.3) - activemodel (= 5.2.3) - activesupport (= 5.2.3) - arel (>= 9.0) - activestorage (5.2.3) - actionpack (= 5.2.3) - activerecord (= 5.2.3) - marcel (~> 0.3.1) - activesupport (5.2.3) - concurrent-ruby (~> 1.0, >= 1.0.2) - i18n (>= 0.7, < 2) - minitest (~> 5.1) - tzinfo (~> 1.1) - appraisal (2.2.0) - bundler - rake - thor (>= 0.14.0) - arel (9.0.0) - builder (3.2.4) - byebug (11.1.3) - coderay (1.1.2) - concurrent-ruby (1.1.6) - connection_pool (2.2.2) - crass (1.0.6) - diff-lcs (1.3) - erubi (1.9.0) - globalid (0.4.2) - activesupport (>= 4.2.0) - i18n (0.9.5) - concurrent-ruby (~> 1.0) - loofah (2.5.0) - crass (~> 1.0.2) - nokogiri (>= 1.5.9) - mail (2.7.1) - mini_mime (>= 0.1.1) - marcel (0.3.3) - mimemagic (~> 0.3.2) - method_source (1.0.0) - mimemagic (0.3.5) - mini_mime (1.0.2) - mini_portile2 (2.1.0) - minitest (5.14.1) - nio4r (2.3.1) - nokogiri (1.7.2) - mini_portile2 (~> 2.1.0) - pg (1.2.3) - pry (0.13.1) - coderay (~> 1.1) - method_source (~> 1.0) - pry-byebug (3.9.0) - byebug (~> 11.0) - pry (~> 0.13.0) - rack (2.2.2) - rack-protection (2.0.8.1) - rack - rack-test (1.1.0) - rack (>= 1.0, < 3) - rails (5.2.3) - actioncable (= 5.2.3) - actionmailer (= 5.2.3) - actionpack (= 5.2.3) - actionview (= 5.2.3) - activejob (= 5.2.3) - activemodel (= 5.2.3) - activerecord (= 5.2.3) - activestorage (= 5.2.3) - activesupport (= 5.2.3) - bundler (>= 1.3.0) - railties (= 5.2.3) - sprockets-rails (>= 2.0.0) - rails-dom-testing (2.0.3) - activesupport (>= 4.2.0) - nokogiri (>= 1.6) - rails-html-sanitizer (1.3.0) - loofah (~> 2.3) - railties (5.2.3) - actionpack (= 5.2.3) - activesupport (= 5.2.3) - method_source - rake (>= 0.8.7) - thor (>= 0.19.0, < 2.0) - rake (12.0.0) - redis (3.3.3) - request_store (1.5.0) - rack (>= 1.4) - rspec (3.9.0) - rspec-core (~> 3.9.0) - rspec-expectations (~> 3.9.0) - rspec-mocks (~> 3.9.0) - rspec-core (3.9.2) - rspec-support (~> 3.9.3) - rspec-expectations (3.9.2) - diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.9.0) - rspec-mocks (3.9.1) - diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.9.0) - rspec-rails (4.0.1) - actionpack (>= 4.2) - activesupport (>= 4.2) - railties (>= 4.2) - rspec-core (~> 3.9) - rspec-expectations (~> 3.9) - rspec-mocks (~> 3.9) - rspec-support (~> 3.9) - rspec-support (3.9.3) - sidekiq (5.0.4) - concurrent-ruby (~> 1.0) - connection_pool (~> 2.2, >= 2.2.0) - rack-protection (>= 1.5.0) - redis (~> 3.3, >= 3.3.3) - sprockets (3.7.2) - concurrent-ruby (~> 1.0) - rack (> 1, < 3) - sprockets-rails (3.2.1) - actionpack (>= 4.0) - activesupport (>= 4.0) - sprockets (>= 3.0.0) - thor (1.0.1) - thread_safe (0.3.6) - tzinfo (1.2.7) - thread_safe (~> 0.1) - websocket-driver (0.7.2) - websocket-extensions (>= 0.1.0) - websocket-extensions (0.1.4) - -PLATFORMS - ruby - -DEPENDENCIES - activerecord-multi-tenant! - appraisal - byebug (~> 11.0) - i18n (~> 0.9.5) - nio4r (~> 2.3.1) - nokogiri (~> 1.7.1) - pg - pry - pry-byebug (= 3.9.0) - rails (~> 5.2.0) - rake (= 12.0.0) - redis (= 3.3.3) - rspec (>= 3.0) - rspec-rails - sidekiq - sprockets (~> 3.7.1) - thor - -BUNDLED WITH - 2.1.4 diff --git a/gemfiles/rails_6.0.gemfile b/gemfiles/rails_6.0.gemfile deleted file mode 100644 index def1fb38..00000000 --- a/gemfiles/rails_6.0.gemfile +++ /dev/null @@ -1,8 +0,0 @@ -# This file was generated by Appraisal - -source "https://rubygems.org" - -gem "appraisal" -gem "rails", "~> 6.0.3" - -gemspec path: "../" diff --git a/gemfiles/rails_6.0.gemfile.lock b/gemfiles/rails_6.0.gemfile.lock deleted file mode 100644 index 4710efad..00000000 --- a/gemfiles/rails_6.0.gemfile.lock +++ /dev/null @@ -1,198 +0,0 @@ -PATH - remote: .. - specs: - activerecord-multi-tenant (1.1.1) - rails (>= 4.2) - request_store (>= 1.0.5) - -GEM - remote: https://rubygems.org/ - specs: - actioncable (6.0.3.1) - actionpack (= 6.0.3.1) - nio4r (~> 2.0) - websocket-driver (>= 0.6.1) - actionmailbox (6.0.3.1) - actionpack (= 6.0.3.1) - activejob (= 6.0.3.1) - activerecord (= 6.0.3.1) - activestorage (= 6.0.3.1) - activesupport (= 6.0.3.1) - mail (>= 2.7.1) - actionmailer (6.0.3.1) - actionpack (= 6.0.3.1) - actionview (= 6.0.3.1) - activejob (= 6.0.3.1) - mail (~> 2.5, >= 2.5.4) - rails-dom-testing (~> 2.0) - actionpack (6.0.3.1) - actionview (= 6.0.3.1) - activesupport (= 6.0.3.1) - rack (~> 2.0, >= 2.0.8) - rack-test (>= 0.6.3) - rails-dom-testing (~> 2.0) - rails-html-sanitizer (~> 1.0, >= 1.2.0) - actiontext (6.0.3.1) - actionpack (= 6.0.3.1) - activerecord (= 6.0.3.1) - activestorage (= 6.0.3.1) - activesupport (= 6.0.3.1) - nokogiri (>= 1.8.5) - actionview (6.0.3.1) - activesupport (= 6.0.3.1) - builder (~> 3.1) - erubi (~> 1.4) - rails-dom-testing (~> 2.0) - rails-html-sanitizer (~> 1.1, >= 1.2.0) - activejob (6.0.3.1) - activesupport (= 6.0.3.1) - globalid (>= 0.3.6) - activemodel (6.0.3.1) - activesupport (= 6.0.3.1) - activerecord (6.0.3.1) - activemodel (= 6.0.3.1) - activesupport (= 6.0.3.1) - activestorage (6.0.3.1) - actionpack (= 6.0.3.1) - activejob (= 6.0.3.1) - activerecord (= 6.0.3.1) - marcel (~> 0.3.1) - activesupport (6.0.3.1) - concurrent-ruby (~> 1.0, >= 1.0.2) - i18n (>= 0.7, < 2) - minitest (~> 5.1) - tzinfo (~> 1.1) - zeitwerk (~> 2.2, >= 2.2.2) - appraisal (2.2.0) - bundler - rake - thor (>= 0.14.0) - builder (3.2.4) - byebug (11.1.3) - coderay (1.1.2) - concurrent-ruby (1.1.6) - connection_pool (2.2.2) - crass (1.0.6) - diff-lcs (1.3) - erubi (1.9.0) - globalid (0.4.2) - activesupport (>= 4.2.0) - i18n (1.8.2) - concurrent-ruby (~> 1.0) - loofah (2.5.0) - crass (~> 1.0.2) - nokogiri (>= 1.5.9) - mail (2.7.1) - mini_mime (>= 0.1.1) - marcel (0.3.3) - mimemagic (~> 0.3.2) - method_source (1.0.0) - mimemagic (0.3.5) - mini_mime (1.0.2) - mini_portile2 (2.4.0) - minitest (5.14.1) - nio4r (2.5.2) - nokogiri (1.10.9) - mini_portile2 (~> 2.4.0) - pg (1.2.3) - pry (0.13.1) - coderay (~> 1.1) - method_source (~> 1.0) - pry-byebug (3.9.0) - byebug (~> 11.0) - pry (~> 0.13.0) - rack (2.2.2) - rack-protection (2.0.8.1) - rack - rack-test (1.1.0) - rack (>= 1.0, < 3) - rails (6.0.3.1) - actioncable (= 6.0.3.1) - actionmailbox (= 6.0.3.1) - actionmailer (= 6.0.3.1) - actionpack (= 6.0.3.1) - actiontext (= 6.0.3.1) - actionview (= 6.0.3.1) - activejob (= 6.0.3.1) - activemodel (= 6.0.3.1) - activerecord (= 6.0.3.1) - activestorage (= 6.0.3.1) - activesupport (= 6.0.3.1) - bundler (>= 1.3.0) - railties (= 6.0.3.1) - sprockets-rails (>= 2.0.0) - rails-dom-testing (2.0.3) - activesupport (>= 4.2.0) - nokogiri (>= 1.6) - rails-html-sanitizer (1.3.0) - loofah (~> 2.3) - railties (6.0.3.1) - actionpack (= 6.0.3.1) - activesupport (= 6.0.3.1) - method_source - rake (>= 0.8.7) - thor (>= 0.20.3, < 2.0) - rake (13.0.1) - redis (4.1.4) - request_store (1.5.0) - rack (>= 1.4) - rspec (3.9.0) - rspec-core (~> 3.9.0) - rspec-expectations (~> 3.9.0) - rspec-mocks (~> 3.9.0) - rspec-core (3.9.2) - rspec-support (~> 3.9.3) - rspec-expectations (3.9.2) - diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.9.0) - rspec-mocks (3.9.1) - diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.9.0) - rspec-rails (4.0.1) - actionpack (>= 4.2) - activesupport (>= 4.2) - railties (>= 4.2) - rspec-core (~> 3.9) - rspec-expectations (~> 3.9) - rspec-mocks (~> 3.9) - rspec-support (~> 3.9) - rspec-support (3.9.3) - sidekiq (6.0.7) - connection_pool (>= 2.2.2) - rack (~> 2.0) - rack-protection (>= 2.0.0) - redis (>= 4.1.0) - sprockets (4.0.0) - concurrent-ruby (~> 1.0) - rack (> 1, < 3) - sprockets-rails (3.2.1) - actionpack (>= 4.0) - activesupport (>= 4.0) - sprockets (>= 3.0.0) - thor (1.0.1) - thread_safe (0.3.6) - tzinfo (1.2.7) - thread_safe (~> 0.1) - websocket-driver (0.7.2) - websocket-extensions (>= 0.1.0) - websocket-extensions (0.1.4) - zeitwerk (2.3.0) - -PLATFORMS - ruby - -DEPENDENCIES - activerecord-multi-tenant! - appraisal - pg - pry - pry-byebug - rails (~> 6.0.3) - rake - rspec (>= 3.0) - rspec-rails - sidekiq - thor - -BUNDLED WITH - 2.1.4 diff --git a/gemfiles/rails_6.1.gemfile b/gemfiles/rails_6.1.gemfile deleted file mode 100644 index 9b9a89ff..00000000 --- a/gemfiles/rails_6.1.gemfile +++ /dev/null @@ -1,8 +0,0 @@ -# This file was generated by Appraisal - -source "https://rubygems.org" - -gem "appraisal" -gem "rails", "~> 6.1.0" - -gemspec path: "../" diff --git a/gemfiles/rails_6.1.gemfile.lock b/gemfiles/rails_6.1.gemfile.lock deleted file mode 100644 index a4c29d39..00000000 --- a/gemfiles/rails_6.1.gemfile.lock +++ /dev/null @@ -1,198 +0,0 @@ -PATH - remote: .. - specs: - activerecord-multi-tenant (1.1.1) - rails (>= 4.2) - request_store (>= 1.0.5) - -GEM - remote: https://rubygems.org/ - specs: - actioncable (6.1.0) - actionpack (= 6.1.0) - activesupport (= 6.1.0) - nio4r (~> 2.0) - websocket-driver (>= 0.6.1) - actionmailbox (6.1.0) - actionpack (= 6.1.0) - activejob (= 6.1.0) - activerecord (= 6.1.0) - activestorage (= 6.1.0) - activesupport (= 6.1.0) - mail (>= 2.7.1) - actionmailer (6.1.0) - actionpack (= 6.1.0) - actionview (= 6.1.0) - activejob (= 6.1.0) - activesupport (= 6.1.0) - mail (~> 2.5, >= 2.5.4) - rails-dom-testing (~> 2.0) - actionpack (6.1.0) - actionview (= 6.1.0) - activesupport (= 6.1.0) - rack (~> 2.0, >= 2.0.9) - rack-test (>= 0.6.3) - rails-dom-testing (~> 2.0) - rails-html-sanitizer (~> 1.0, >= 1.2.0) - actiontext (6.1.0) - actionpack (= 6.1.0) - activerecord (= 6.1.0) - activestorage (= 6.1.0) - activesupport (= 6.1.0) - nokogiri (>= 1.8.5) - actionview (6.1.0) - activesupport (= 6.1.0) - builder (~> 3.1) - erubi (~> 1.4) - rails-dom-testing (~> 2.0) - rails-html-sanitizer (~> 1.1, >= 1.2.0) - activejob (6.1.0) - activesupport (= 6.1.0) - globalid (>= 0.3.6) - activemodel (6.1.0) - activesupport (= 6.1.0) - activerecord (6.1.0) - activemodel (= 6.1.0) - activesupport (= 6.1.0) - activestorage (6.1.0) - actionpack (= 6.1.0) - activejob (= 6.1.0) - activerecord (= 6.1.0) - activesupport (= 6.1.0) - marcel (~> 0.3.1) - mimemagic (~> 0.3.2) - activesupport (6.1.0) - concurrent-ruby (~> 1.0, >= 1.0.2) - i18n (>= 1.6, < 2) - minitest (>= 5.1) - tzinfo (~> 2.0) - zeitwerk (~> 2.3) - appraisal (2.3.0) - bundler - rake - thor (>= 0.14.0) - builder (3.2.4) - byebug (11.1.3) - coderay (1.1.3) - concurrent-ruby (1.1.7) - connection_pool (2.2.3) - crass (1.0.6) - diff-lcs (1.4.4) - erubi (1.10.0) - globalid (0.4.2) - activesupport (>= 4.2.0) - i18n (1.8.5) - concurrent-ruby (~> 1.0) - loofah (2.8.0) - crass (~> 1.0.2) - nokogiri (>= 1.5.9) - mail (2.7.1) - mini_mime (>= 0.1.1) - marcel (0.3.3) - mimemagic (~> 0.3.2) - method_source (1.0.0) - mimemagic (0.3.5) - mini_mime (1.0.2) - mini_portile2 (2.4.0) - minitest (5.14.2) - nio4r (2.5.4) - nokogiri (1.10.10) - mini_portile2 (~> 2.4.0) - pg (1.2.3) - pry (0.13.1) - coderay (~> 1.1) - method_source (~> 1.0) - pry-byebug (3.9.0) - byebug (~> 11.0) - pry (~> 0.13.0) - rack (2.2.3) - rack-test (1.1.0) - rack (>= 1.0, < 3) - rails (6.1.0) - actioncable (= 6.1.0) - actionmailbox (= 6.1.0) - actionmailer (= 6.1.0) - actionpack (= 6.1.0) - actiontext (= 6.1.0) - actionview (= 6.1.0) - activejob (= 6.1.0) - activemodel (= 6.1.0) - activerecord (= 6.1.0) - activestorage (= 6.1.0) - activesupport (= 6.1.0) - bundler (>= 1.15.0) - railties (= 6.1.0) - sprockets-rails (>= 2.0.0) - rails-dom-testing (2.0.3) - activesupport (>= 4.2.0) - nokogiri (>= 1.6) - rails-html-sanitizer (1.3.0) - loofah (~> 2.3) - railties (6.1.0) - actionpack (= 6.1.0) - activesupport (= 6.1.0) - method_source - rake (>= 0.8.7) - thor (~> 1.0) - rake (13.0.3) - redis (4.2.5) - request_store (1.5.0) - rack (>= 1.4) - rspec (3.10.0) - rspec-core (~> 3.10.0) - rspec-expectations (~> 3.10.0) - rspec-mocks (~> 3.10.0) - rspec-core (3.10.1) - rspec-support (~> 3.10.0) - rspec-expectations (3.10.1) - diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.10.0) - rspec-mocks (3.10.1) - diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.10.0) - rspec-rails (4.0.2) - actionpack (>= 4.2) - activesupport (>= 4.2) - railties (>= 4.2) - rspec-core (~> 3.10) - rspec-expectations (~> 3.10) - rspec-mocks (~> 3.10) - rspec-support (~> 3.10) - rspec-support (3.10.1) - sidekiq (6.1.2) - connection_pool (>= 2.2.2) - rack (~> 2.0) - redis (>= 4.2.0) - sprockets (4.0.2) - concurrent-ruby (~> 1.0) - rack (> 1, < 3) - sprockets-rails (3.2.2) - actionpack (>= 4.0) - activesupport (>= 4.0) - sprockets (>= 3.0.0) - thor (1.0.1) - tzinfo (2.0.4) - concurrent-ruby (~> 1.0) - websocket-driver (0.7.3) - websocket-extensions (>= 0.1.0) - websocket-extensions (0.1.5) - zeitwerk (2.4.2) - -PLATFORMS - ruby - -DEPENDENCIES - activerecord-multi-tenant! - appraisal - pg - pry - pry-byebug - rails (~> 6.1.0) - rake - rspec (>= 3.0) - rspec-rails - sidekiq - thor - -BUNDLED WITH - 2.1.4 diff --git a/lib/activerecord-multi-tenant.rb b/lib/activerecord-multi-tenant.rb index f1e306a1..07fb6f5a 100644 --- a/lib/activerecord-multi-tenant.rb +++ b/lib/activerecord-multi-tenant.rb @@ -1,13 +1,3 @@ -if Object.const_defined?(:ActionController) - require_relative 'activerecord-multi-tenant/controller_extensions' -end -require_relative 'activerecord-multi-tenant/copy_from_client' -require_relative 'activerecord-multi-tenant/fast_truncate' -require_relative 'activerecord-multi-tenant/migrations' -require_relative 'activerecord-multi-tenant/model_extensions' -require_relative 'activerecord-multi-tenant/multi_tenant' -require_relative 'activerecord-multi-tenant/query_rewriter' -require_relative 'activerecord-multi-tenant/query_monitor' -require_relative 'activerecord-multi-tenant/version' -require_relative 'activerecord-multi-tenant/with_lock' -require_relative 'activerecord-multi-tenant/persistence_extension' +# frozen_string_literal: true + +require 'activerecord_multi_tenant' diff --git a/lib/activerecord-multi-tenant/arel_visitors_depth_first.rb b/lib/activerecord-multi-tenant/arel_visitors_depth_first.rb index 5a3a18d9..a0c0f3b2 100644 --- a/lib/activerecord-multi-tenant/arel_visitors_depth_first.rb +++ b/lib/activerecord-multi-tenant/arel_visitors_depth_first.rb @@ -1,200 +1,209 @@ +# frozen_string_literal: true + module MultiTenant class ArelVisitorsDepthFirst < Arel::Visitors::Visitor def initialize(block = nil) - @block = block || Proc.new + @block = block || proc super() end private - def visit(o, _ = nil) - super - @block.call o - end + def visit(obj, _ = nil) + super + @block.call obj + end - def unary(o) - visit o.expr - end - alias :visit_Arel_Nodes_Else :unary - alias :visit_Arel_Nodes_Group :unary - alias :visit_Arel_Nodes_Cube :unary - alias :visit_Arel_Nodes_RollUp :unary - alias :visit_Arel_Nodes_GroupingSet :unary - alias :visit_Arel_Nodes_GroupingElement :unary - alias :visit_Arel_Nodes_Grouping :unary - alias :visit_Arel_Nodes_Having :unary - alias :visit_Arel_Nodes_Lateral :unary - alias :visit_Arel_Nodes_Limit :unary - alias :visit_Arel_Nodes_Not :unary - alias :visit_Arel_Nodes_Offset :unary - alias :visit_Arel_Nodes_On :unary - alias :visit_Arel_Nodes_Ordering :unary - alias :visit_Arel_Nodes_Ascending :unary - alias :visit_Arel_Nodes_Descending :unary - alias :visit_Arel_Nodes_UnqualifiedColumn :unary - alias :visit_Arel_Nodes_OptimizerHints :unary - alias :visit_Arel_Nodes_ValuesList :unary - - def function(o) - visit o.expressions - visit o.alias - visit o.distinct - end - alias :visit_Arel_Nodes_Avg :function - alias :visit_Arel_Nodes_Exists :function - alias :visit_Arel_Nodes_Max :function - alias :visit_Arel_Nodes_Min :function - alias :visit_Arel_Nodes_Sum :function - - def visit_Arel_Nodes_NamedFunction(o) - visit o.name - visit o.expressions - visit o.distinct - visit o.alias - end + def unary(obj) + visit obj.expr + end + alias visit_Arel_Nodes_Else unary + alias visit_Arel_Nodes_Group unary + alias visit_Arel_Nodes_Cube unary + alias visit_Arel_Nodes_RollUp unary + alias visit_Arel_Nodes_GroupingSet unary + alias visit_Arel_Nodes_GroupingElement unary + alias visit_Arel_Nodes_Grouping unary + alias visit_Arel_Nodes_Having unary + alias visit_Arel_Nodes_Lateral unary + alias visit_Arel_Nodes_Limit unary + alias visit_Arel_Nodes_Not unary + alias visit_Arel_Nodes_Offset unary + alias visit_Arel_Nodes_On unary + alias visit_Arel_Nodes_Ordering unary + alias visit_Arel_Nodes_Ascending unary + alias visit_Arel_Nodes_Descending unary + alias visit_Arel_Nodes_UnqualifiedColumn unary + alias visit_Arel_Nodes_OptimizerHints unary + alias visit_Arel_Nodes_ValuesList unary + + def function(obj) + visit obj.expressions + visit obj.alias + visit obj.distinct + end + alias visit_Arel_Nodes_Avg function + alias visit_Arel_Nodes_Exists function + alias visit_Arel_Nodes_Max function + alias visit_Arel_Nodes_Min function + alias visit_Arel_Nodes_Sum function + + # rubocop:disable Naming/MethodName + + def visit_Arel_Nodes_NamedFunction(obj) + visit obj.name + visit obj.expressions + visit obj.distinct + visit obj.alias + end - def visit_Arel_Nodes_Count(o) - visit o.expressions - visit o.alias - visit o.distinct - end + def visit_Arel_Nodes_Count(obj) + visit obj.expressions + visit obj.alias + visit obj.distinct + end - def visit_Arel_Nodes_Case(o) - visit o.case - visit o.conditions - visit o.default - end + def visit_Arel_Nodes_Case(obj) + visit obj.case + visit obj.conditions + visit obj.default + end - def nary(o) - o.children.each { |child| visit child } - end - alias :visit_Arel_Nodes_And :nary + def nary(obj) + obj.children.each { |child| visit child } + end + alias visit_Arel_Nodes_And nary - def binary(o) - visit o.left - visit o.right - end - alias :visit_Arel_Nodes_As :binary - alias :visit_Arel_Nodes_Assignment :binary - alias :visit_Arel_Nodes_Between :binary - alias :visit_Arel_Nodes_Concat :binary - alias :visit_Arel_Nodes_DeleteStatement :binary - alias :visit_Arel_Nodes_DoesNotMatch :binary - alias :visit_Arel_Nodes_Equality :binary - alias :visit_Arel_Nodes_FullOuterJoin :binary - alias :visit_Arel_Nodes_GreaterThan :binary - alias :visit_Arel_Nodes_GreaterThanOrEqual :binary - alias :visit_Arel_Nodes_In :binary - alias :visit_Arel_Nodes_InfixOperation :binary - alias :visit_Arel_Nodes_JoinSource :binary - alias :visit_Arel_Nodes_InnerJoin :binary - alias :visit_Arel_Nodes_LessThan :binary - alias :visit_Arel_Nodes_LessThanOrEqual :binary - alias :visit_Arel_Nodes_Matches :binary - alias :visit_Arel_Nodes_NotEqual :binary - alias :visit_Arel_Nodes_NotIn :binary - alias :visit_Arel_Nodes_NotRegexp :binary - alias :visit_Arel_Nodes_IsNotDistinctFrom :binary - alias :visit_Arel_Nodes_IsDistinctFrom :binary - alias :visit_Arel_Nodes_Or :binary - alias :visit_Arel_Nodes_OuterJoin :binary - alias :visit_Arel_Nodes_Regexp :binary - alias :visit_Arel_Nodes_RightOuterJoin :binary - alias :visit_Arel_Nodes_TableAlias :binary - alias :visit_Arel_Nodes_When :binary - - def visit_Arel_Nodes_StringJoin(o) - visit o.left - end + def binary(obj) + visit obj.left + visit obj.right + end + alias visit_Arel_Nodes_As binary + alias visit_Arel_Nodes_Assignment binary + alias visit_Arel_Nodes_Between binary + alias visit_Arel_Nodes_Concat binary + alias visit_Arel_Nodes_DeleteStatement binary + alias visit_Arel_Nodes_DoesNotMatch binary + alias visit_Arel_Nodes_Equality binary + alias visit_Arel_Nodes_FullOuterJoin binary + alias visit_Arel_Nodes_GreaterThan binary + alias visit_Arel_Nodes_GreaterThanOrEqual binary + alias visit_Arel_Nodes_In binary + alias visit_Arel_Nodes_InfixOperation binary + alias visit_Arel_Nodes_JoinSource binary + alias visit_Arel_Nodes_InnerJoin binary + alias visit_Arel_Nodes_LessThan binary + alias visit_Arel_Nodes_LessThanOrEqual binary + alias visit_Arel_Nodes_Matches binary + alias visit_Arel_Nodes_NotEqual binary + alias visit_Arel_Nodes_NotIn binary + alias visit_Arel_Nodes_NotRegexp binary + alias visit_Arel_Nodes_IsNotDistinctFrom binary + alias visit_Arel_Nodes_IsDistinctFrom binary + alias visit_Arel_Nodes_Or binary + alias visit_Arel_Nodes_OuterJoin binary + alias visit_Arel_Nodes_Regexp binary + alias visit_Arel_Nodes_RightOuterJoin binary + alias visit_Arel_Nodes_TableAlias binary + alias visit_Arel_Nodes_When binary + + def visit_Arel_Nodes_StringJoin(obj) + visit obj.left + end - def visit_Arel_Attribute(o) - visit o.relation - visit o.name - end - alias :visit_Arel_Attributes_Integer :visit_Arel_Attribute - alias :visit_Arel_Attributes_Float :visit_Arel_Attribute - alias :visit_Arel_Attributes_String :visit_Arel_Attribute - alias :visit_Arel_Attributes_Time :visit_Arel_Attribute - alias :visit_Arel_Attributes_Boolean :visit_Arel_Attribute - alias :visit_Arel_Attributes_Attribute :visit_Arel_Attribute - alias :visit_Arel_Attributes_Decimal :visit_Arel_Attribute - - def visit_Arel_Table(o) - visit o.name - end + def visit_Arel_Attribute(obj) + visit obj.relation + visit obj.name + end + alias visit_Arel_Attributes_Integer visit_Arel_Attribute + alias visit_Arel_Attributes_Float visit_Arel_Attribute + alias visit_Arel_Attributes_String visit_Arel_Attribute + alias visit_Arel_Attributes_Time visit_Arel_Attribute + alias visit_Arel_Attributes_Boolean visit_Arel_Attribute + alias visit_Arel_Attributes_Attribute visit_Arel_Attribute + alias visit_Arel_Attributes_Decimal visit_Arel_Attribute + + def visit_Arel_Table(obj) + visit obj.name + end - def terminal(o) - end - alias :visit_ActiveSupport_Multibyte_Chars :terminal - alias :visit_ActiveSupport_StringInquirer :terminal - alias :visit_Arel_Nodes_Lock :terminal - alias :visit_Arel_Nodes_Node :terminal - alias :visit_Arel_Nodes_SqlLiteral :terminal - alias :visit_Arel_Nodes_BindParam :terminal - alias :visit_Arel_Nodes_Window :terminal - alias :visit_Arel_Nodes_True :terminal - alias :visit_Arel_Nodes_False :terminal - alias :visit_BigDecimal :terminal - alias :visit_Class :terminal - alias :visit_Date :terminal - alias :visit_DateTime :terminal - alias :visit_FalseClass :terminal - alias :visit_Float :terminal - alias :visit_Integer :terminal - alias :visit_NilClass :terminal - alias :visit_String :terminal - alias :visit_Symbol :terminal - alias :visit_Time :terminal - alias :visit_TrueClass :terminal - - def visit_Arel_Nodes_InsertStatement(o) - visit o.relation - visit o.columns - visit o.values - end + def terminal(obj); end + alias visit_ActiveSupport_Multibyte_Chars terminal + alias visit_ActiveSupport_StringInquirer terminal + alias visit_Arel_Nodes_Lock terminal + alias visit_Arel_Nodes_Node terminal + alias visit_Arel_Nodes_SqlLiteral terminal + alias visit_Arel_Nodes_BindParam terminal + alias visit_Arel_Nodes_Window terminal + alias visit_Arel_Nodes_True terminal + alias visit_Arel_Nodes_False terminal + alias visit_BigDecimal terminal + alias visit_Class terminal + alias visit_Date terminal + alias visit_DateTime terminal + alias visit_FalseClass terminal + alias visit_Float terminal + alias visit_Integer terminal + alias visit_NilClass terminal + alias visit_String terminal + alias visit_Symbol terminal + alias visit_Time terminal + alias visit_TrueClass terminal + + def visit_Arel_Nodes_InsertStatement(obj) + visit obj.relation + visit obj.columns + visit obj.values + end - def visit_Arel_Nodes_SelectCore(o) - visit o.projections - visit o.source - visit o.wheres - visit o.groups - visit o.windows - visit o.havings - end + def visit_Arel_Nodes_SelectCore(obj) + visit obj.projections + visit obj.source + visit obj.wheres + visit obj.groups + visit obj.windows + visit obj.havings + end - def visit_Arel_Nodes_SelectStatement(o) - visit o.cores - visit o.orders - visit o.limit - visit o.lock - visit o.offset - end + def visit_Arel_Nodes_SelectStatement(obj) + visit obj.cores + visit obj.orders + visit obj.limit + visit obj.lock + visit obj.offset + end - def visit_Arel_Nodes_UpdateStatement(o) - visit o.relation - visit o.values - visit o.wheres - visit o.orders - visit o.limit - end + def visit_Arel_Nodes_UpdateStatement(obj) + visit obj.relation + visit obj.values + visit obj.wheres + visit obj.orders + visit obj.limit + end - def visit_Arel_Nodes_Comment(o) - visit o.values - end + def visit_Arel_Nodes_Comment(obj) + visit obj.values + end - def visit_Array(o) - o.each { |i| visit i } - end - alias :visit_Set :visit_Array + def visit_Array(obj) + obj.each { |i| visit i } + end + alias visit_Set visit_Array - def visit_Hash(o) - o.each { |k, v| visit(k); visit(v) } + def visit_Hash(obj) + obj.each do |k, v| + visit(k) + visit(v) end + end - DISPATCH = dispatch_cache + DISPATCH = dispatch_cache - def get_dispatch_cache - DISPATCH - end + # rubocop:disable Naming/AccessorMethodName + def get_dispatch_cache + DISPATCH + end + # rubocop:enable Naming/AccessorMethodName + # rubocop:enable Naming/MethodName end end diff --git a/lib/activerecord-multi-tenant/controller_extensions.rb b/lib/activerecord-multi-tenant/controller_extensions.rb index 4586f02f..cf496561 100644 --- a/lib/activerecord-multi-tenant/controller_extensions.rb +++ b/lib/activerecord-multi-tenant/controller_extensions.rb @@ -1,16 +1,22 @@ +# frozen_string_literal: true + +# Extension to the controller to allow setting the current tenant +# set_current_tenant and current_tenant methods are introduced +# to set and get the current tenant in the controllers that uses +# the MultiTenant module. module MultiTenant module ControllerExtensions def set_current_tenant_through_filter - self.class_eval do - if respond_to?(:helper_method) - helper_method :current_tenant - end + class_eval do + helper_method :current_tenant if respond_to?(:helper_method) private + # rubocop:disable Naming/AccessorMethodName def set_current_tenant(current_tenant_object) MultiTenant.current_tenant = current_tenant_object end + # rubocop:enable Naming/AccessorMethodName def current_tenant MultiTenant.current_tenant @@ -20,6 +26,11 @@ def current_tenant end end +# This block is executed when the file is loaded and +# makes the base class; ActionController::Base to +# extend the ControllerExtensions module. +# This will add the set_current_tenant and current_tenant +# in all the controllers that inherit from ActionController::Base ActiveSupport.on_load(:action_controller) do |base| base.extend MultiTenant::ControllerExtensions end diff --git a/lib/activerecord-multi-tenant/copy_from_client.rb b/lib/activerecord-multi-tenant/copy_from_client.rb index e6910790..8df5b606 100644 --- a/lib/activerecord-multi-tenant/copy_from_client.rb +++ b/lib/activerecord-multi-tenant/copy_from_client.rb @@ -1,4 +1,9 @@ +# frozen_string_literal: true + module MultiTenant + # Designed to be mixed into an ActiveRecord model to provide + # a copy_from_client method that allows for efficient bulk insertion of + # data into a PostgreSQL database using the COPY command class CopyFromClientHelper attr_reader :count @@ -9,7 +14,7 @@ def initialize(conn, column_types) end def <<(row) - row = row.map.with_index { |val, idx| @column_types[idx].type_cast_for_database(val) } + row = row.map.with_index { |val, idx| @column_types[idx].serialize(val) } @conn.put_copy_data(row) @count += 1 end @@ -18,7 +23,7 @@ def <<(row) module CopyFromClient def copy_from_client(columns, &block) conn = connection.raw_connection - column_types = columns.map { |c| columns_hash[c.to_s] } + column_types = columns.map { |c| type_for_attribute(c.to_s) } helper = MultiTenant::CopyFromClientHelper.new(conn, column_types) conn.copy_data %{COPY #{quoted_table_name}("#{columns.join('","')}") FROM STDIN}, PG::TextEncoder::CopyRow.new do block.call helper @@ -28,6 +33,7 @@ def copy_from_client(columns, &block) end end +# Add copy_from_client to ActiveRecord::Base ActiveSupport.on_load(:active_record) do |base| base.extend(MultiTenant::CopyFromClient) end diff --git a/lib/activerecord-multi-tenant/fast_truncate.rb b/lib/activerecord-multi-tenant/fast_truncate.rb index 160b4e55..fdd5d97d 100644 --- a/lib/activerecord-multi-tenant/fast_truncate.rb +++ b/lib/activerecord-multi-tenant/fast_truncate.rb @@ -1,5 +1,8 @@ +# frozen_string_literal: true + # Truncates only the tables that have been modified, according to sequence # values +# Faster alternative to DatabaseCleaner.clean_with(:truncation, pre_count: true) module MultiTenant module FastTruncate def self.run(exclude: ['schema_migrations']) @@ -13,7 +16,8 @@ def self.run(exclude: ['schema_migrations']) needs_truncate boolean; BEGIN FOR t IN SELECT schemaname, tablename FROM pg_tables WHERE schemaname = 'public' AND tablename NOT IN (%s) LOOP - EXECUTE 'SELECT EXISTS (SELECT * from pg_class c WHERE c.relkind = ''S'' AND c.relname=''' || t.tablename || '_id_seq'')' into seq_exists; + EXECUTE 'SELECT EXISTS (SELECT * from pg_class c WHERE c.relkind = ''S'' + AND c.relname=''' || t.tablename || '_id_seq'')' into seq_exists; IF seq_exists THEN EXECUTE 'SELECT is_called FROM ' || t.tablename || '_id_seq' INTO needs_truncate; ELSE @@ -28,7 +32,7 @@ def self.run(exclude: ['schema_migrations']) IF array_length(tables, 1) > 0 THEN EXECUTE 'TRUNCATE TABLE ' || array_to_string(tables, ', ') || ' RESTART IDENTITY CASCADE'; END IF; - END$$;), exclude.map { |t| "'" + t + "'" }.join('\n')) + END$$;), exclude.map { |t| "'#{t}'" }.join('\n')) end end end diff --git a/lib/activerecord-multi-tenant/habtm.rb b/lib/activerecord-multi-tenant/habtm.rb new file mode 100644 index 00000000..31618451 --- /dev/null +++ b/lib/activerecord-multi-tenant/habtm.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +# This module extension is a monkey patch to the ActiveRecord::Associations::ClassMethods module. +# It overrides the has_and_belongs_to_many method to add the tenant_id to the join table if the +# tenant_enabled option is set to true. + +module ActiveRecord + module Associations + module ClassMethods + # rubocop:disable Naming/PredicateName + def has_and_belongs_to_many_with_tenant(name, scope = nil, **options, &extension) + # rubocop:enable Naming/PredicateName + has_and_belongs_to_many_without_tenant(name, scope, **options, &extension) + + middle_reflection = _reflections[name.to_s].through_reflection + join_model = middle_reflection.klass + + # get tenant_enabled from options and if it is not set, set it to false + tenant_enabled = options[:tenant_enabled] || false + + return unless tenant_enabled + + tenant_class_name = options[:tenant_class_name] + tenant_column = options[:tenant_column] + + match = tenant_column.match(/(\w+)_id/) + tenant_field_name = match ? match[1] : 'tenant' + + join_model.class_eval do + belongs_to tenant_field_name.to_sym, class_name: tenant_class_name + before_create :tenant_set + + private + + # This method sets the tenant_id on the join table and executes before creation of the join table record. + define_method :tenant_set do + return unless tenant_enabled + raise MultiTenant::MissingTenantError, 'Tenant Id is not set' unless MultiTenant.current_tenant_id + + send("#{tenant_column}=", MultiTenant.current_tenant_id) + end + end + end + + alias has_and_belongs_to_many_without_tenant has_and_belongs_to_many + alias has_and_belongs_to_many has_and_belongs_to_many_with_tenant + end + end +end diff --git a/lib/activerecord-multi-tenant/migrations.rb b/lib/activerecord-multi-tenant/migrations.rb index 25a4ea25..cd069705 100644 --- a/lib/activerecord-multi-tenant/migrations.rb +++ b/lib/activerecord-multi-tenant/migrations.rb @@ -1,13 +1,43 @@ +# frozen_string_literal: true + module MultiTenant module MigrationExtensions def create_distributed_table(table_name, partition_key) return unless citus_version.present? - execute "SELECT create_distributed_table($$#{table_name}$$, $$#{partition_key}$$)" + + reversible do |dir| + dir.up do + execute "SELECT create_distributed_table($$#{table_name}$$, $$#{partition_key}$$)" + end + dir.down do + undistribute_table(table_name) + end + end end def create_reference_table(table_name) return unless citus_version.present? - execute "SELECT create_reference_table($$#{table_name}$$)" + + reversible do |dir| + dir.up do + execute "SELECT create_reference_table($$#{table_name}$$)" + end + dir.down do + undistribute_table(table_name) + end + end + end + + def undistribute_table(table_name) + return unless citus_version.present? + + execute "SELECT undistribute_table($$#{table_name}$$))" + end + + def rebalance_table_shards + return unless citus_version.present? + + execute 'SELECT rebalance_table_shards()' end def execute_on_all_nodes(sql) @@ -18,7 +48,8 @@ def execute_on_all_nodes(sql) execute "SELECT citus_run_on_all_workers($$#{sql}$$)" # initial citus_tools.sql with different names when nil # Do nothing, this is regular Postgres - else # 6.1 and newer + else + # 6.1 and newer execute "SELECT run_command_on_workers($$#{sql}$$)" end end @@ -28,28 +59,73 @@ def enable_extension_on_all_nodes(extension) end def citus_version - execute("SELECT extversion FROM pg_extension WHERE extname = 'citus'").getvalue(0,0).try(:split, '-').try(:first) + execute("SELECT extversion FROM pg_extension WHERE extname = 'citus'").getvalue(0, 0).try(:split, '-').try(:first) rescue ArgumentError => e - raise unless e.message == "invalid tuple number 0" + raise unless e.message == 'invalid tuple number 0' end end end -if defined?(ActiveRecord::Migration) - ActiveRecord::Migration.send(:include, MultiTenant::MigrationExtensions) +ActiveRecord::Migration.include MultiTenant::MigrationExtensions if defined?(ActiveRecord::Migration) + +module MultiTenant + module SchemaStatementsExtensions + def create_table(table_name, options = {}, &block) + ret = super(table_name, **options.except(:partition_key), &block) + if options[:id] != false && options[:partition_key] && options[:partition_key].to_s != 'id' + execute "ALTER TABLE #{table_name} DROP CONSTRAINT #{table_name}_pkey" + execute "ALTER TABLE #{table_name} ADD PRIMARY KEY(\"#{options[:partition_key]}\", id)" + end + ret + end + end end +ActiveRecord::ConnectionAdapters::SchemaStatements.prepend(MultiTenant::SchemaStatementsExtensions) module ActiveRecord - module ConnectionAdapters # :nodoc: - module SchemaStatements - alias :orig_create_table :create_table - def create_table(table_name, options = {}, &block) - ret = orig_create_table(table_name, **options.except(:partition_key), &block) - if options[:partition_key] && options[:partition_key].to_s != 'id' - execute "ALTER TABLE #{table_name} DROP CONSTRAINT #{table_name}_pkey" - execute "ALTER TABLE #{table_name} ADD PRIMARY KEY(\"#{options[:partition_key]}\", id)" + class SchemaDumper + private + + alias initialize_without_citus initialize + + def initialize(connection, options = {}) + initialize_without_citus(connection, options) + + citus_version = + begin + ActiveRecord::Migration.citus_version + rescue StandardError + # Handle the case where this gem is used with MySQL https://github.com/citusdata/activerecord-multi-tenant/issues/166 + nil + end + @distribution_columns = + if citus_version.present? + query_to_execute = <<-SQL.strip + SELECT logicalrelid::regclass AS table_name, + column_to_column_name(logicalrelid, partkey) AS dist_col_name + FROM pg_dist_partition + SQL + @connection.execute(query_to_execute).to_h do |v| + [v['table_name'], v['dist_col_name']] + end + else + {} end - ret + end + + # Support for create_distributed_table & create_reference_table + alias table_without_citus table + + def table(table, stream) + table_without_citus(table, stream) + table_name = remove_prefix_and_suffix(table) + distribution_column = @distribution_columns[table_name] + if distribution_column + stream.puts " create_distributed_table(#{table_name.inspect}, #{distribution_column.inspect})" + stream.puts + elsif @distribution_columns.key?(table_name) + stream.puts " create_reference_table(#{table_name.inspect})" + stream.puts end end end diff --git a/lib/activerecord-multi-tenant/model_extensions.rb b/lib/activerecord-multi-tenant/model_extensions.rb index 369c1718..a2811130 100644 --- a/lib/activerecord-multi-tenant/model_extensions.rb +++ b/lib/activerecord-multi-tenant/model_extensions.rb @@ -1,12 +1,34 @@ +# frozen_string_literal: true + +require_relative 'multi_tenant' + module MultiTenant + # Extension to the model to allow scoping of models to the current tenant. This is done by adding + # the multitenant method to the models that need to be scoped. This method is called in the + # model declaration. + # Adds scoped_by_tenant? partition_key, primary_key and inherited methods to the model module ModelExtensionsClassMethods - DEFAULT_ID_FIELD = 'id'.freeze - + DEFAULT_ID_FIELD = 'id' + # executes when multi_tenant method is called in the model. This method adds the following + # methods to the model that calls it. + # scoped_by_tenant? - returns true if the model is scoped by tenant + # partition_key - returns the partition key for the model + # primary_key - returns the primary key for the model + # def multi_tenant(tenant_name, options = {}) - if to_s.underscore.to_sym == tenant_name + if to_s.underscore.to_sym == tenant_name || (!table_name.nil? && table_name.singularize.to_sym == tenant_name) unless MultiTenant.with_write_only_mode_enabled? # This is the tenant model itself. Workaround for https://github.com/citusdata/citus/issues/687 - before_create -> { self.id ||= self.class.connection.select_value("SELECT nextval('" + [self.class.table_name, self.class.primary_key, 'seq'].join('_') + "'::regclass)") } + before_create lambda { + id = if self.class.columns_hash[self.class.primary_key].type == :uuid + SecureRandom.uuid + else + self.class.connection.select_value( + "SELECT nextval('#{self.class.table_name}_#{self.class.primary_key}_seq'::regclass)" + ) + end + self.id ||= id + } end else class << self @@ -16,75 +38,88 @@ def scoped_by_tenant? # Allow partition_key to be set from a superclass if not already set in this class def partition_key - @partition_key ||= ancestors.detect{ |k| k.instance_variable_get(:@partition_key) } - .try(:instance_variable_get, :@partition_key) + @partition_key ||= ancestors.detect { |k| k.instance_variable_get(:@partition_key) } + .try(:instance_variable_get, :@partition_key) end # Avoid primary_key errors when using composite primary keys (e.g. id, tenant_id) def primary_key - return @primary_key if @primary_key + if defined?(PRIMARY_KEY_NOT_SET) ? !PRIMARY_KEY_NOT_SET.equal?(@primary_key) : @primary_key + return @primary_key + end primary_object_keys = Array.wrap(connection.schema_cache.primary_keys(table_name)) - [partition_key] - if primary_object_keys.size == 1 - @primary_key = primary_object_keys.first - elsif connection.schema_cache.columns_hash(table_name).include? DEFAULT_ID_FIELD - @primary_key = DEFAULT_ID_FIELD - else - # table without a primary key and DEFAULT_ID_FIELD is not present in the table - @primary_key = nil - end + @primary_key = if primary_object_keys.size == 1 + primary_object_keys.first + elsif table_name && + connection.schema_cache.columns_hash(table_name).include?(DEFAULT_ID_FIELD) + DEFAULT_ID_FIELD + end end def inherited(subclass) super - MultiTenant.register_multi_tenant_model(subclass.table_name, subclass) if subclass.table_name + MultiTenant.register_multi_tenant_model(subclass) end end - MultiTenant.register_multi_tenant_model(table_name, self) if table_name + MultiTenant.register_multi_tenant_model(self) @partition_key = options[:partition_key] || MultiTenant.partition_key(tenant_name) partition_key = @partition_key # Create an implicit belongs_to association only if tenant class exists - if MultiTenant.tenant_klass_defined?(tenant_name) - belongs_to tenant_name, **options.slice(:class_name, :inverse_of).merge(foreign_key: options[:partition_key]) + if MultiTenant.tenant_klass_defined?(tenant_name, options) + belongs_to tenant_name, **options.slice(:class_name, :inverse_of, :optional) + .merge(foreign_key: options[:partition_key]) end # New instances should have the tenant set - after_initialize Proc.new { |record| + after_initialize proc { |record| if MultiTenant.current_tenant_id && - (!record.attribute_present?(partition_key) || record.public_send(partition_key.to_sym).nil?) - record.public_send("#{partition_key}=".to_sym, MultiTenant.current_tenant_id) + (!record.attribute_present?(partition_key) || record.public_send(partition_key.to_sym).nil?) + record.public_send(:"#{partition_key}=", MultiTenant.current_tenant_id) end } + # Below block adds the following methods to the model that calls it. + # partition_key= - returns the partition key for the model.class << self 'partition' method defined above + # is the getter method. Here, there is additional check to assure that the tenant id is not changed once set + # tenant_name- returns the name of the tenant model. Its setter and getter methods defined separately + # Getter checks for the tenant association and if it is not loaded, returns the current tenant id set + # in the MultiTenant module to_include = Module.new do define_method "#{partition_key}=" do |tenant_id| - write_attribute("#{partition_key}", tenant_id) + write_attribute(partition_key.to_s, tenant_id) # Rails 5 `attribute_will_change!` uses the attribute-method-call rather than `read_attribute` # and will raise ActiveModel::MissingAttributeError if that column was not selected. # This is rescued as NoMethodError and in MRI attribute_was is assigned an arbitrary Object - # This is still true after the Rails 5.2 refactor was = send("#{partition_key}_was") - was_nil_or_skipped = was.nil? || was.class == Object + was_nil_or_skipped = was.nil? || was.instance_of?(Object) + + if send("#{partition_key}_changed?") && persisted? && !was_nil_or_skipped + raise MultiTenant::TenantIsImmutable + end - raise MultiTenant::TenantIsImmutable if send("#{partition_key}_changed?") && persisted? && !was_nil_or_skipped tenant_id end - if MultiTenant.tenant_klass_defined?(tenant_name) + if MultiTenant.tenant_klass_defined?(tenant_name, options) define_method "#{tenant_name}=" do |model| super(model) - raise MultiTenant::TenantIsImmutable if send("#{partition_key}_changed?") && persisted? && !send("#{partition_key}_was").nil? + if send("#{partition_key}_changed?") && persisted? && !send("#{partition_key}_was").nil? + raise MultiTenant::TenantIsImmutable + end + model end - define_method "#{tenant_name}" do - if !association(tenant_name.to_sym).loaded? && !MultiTenant.current_tenant_is_id? && MultiTenant.current_tenant_id && public_send(partition_key) == MultiTenant.current_tenant_id - return MultiTenant.current_tenant + define_method tenant_name.to_s do + if !association(tenant_name.to_sym).loaded? && !MultiTenant.current_tenant_is_id? && + MultiTenant.current_tenant_id && public_send(partition_key) == MultiTenant.current_tenant_id + MultiTenant.current_tenant else super() end @@ -93,23 +128,28 @@ def inherited(subclass) end include to_include - around_save -> (record, block) { - if persisted? && MultiTenant.current_tenant_id.nil? + # Below blocks sets tenant_id for the current session with the tenant_id of the record + # If the tenant is not set for the `session.After` the save operation current session tenant is set to nil + # If tenant is set for the session, save operation is performed as it is + around_save lambda { |record, block| + record_tenant = record.attribute_was(partition_key) + if persisted? && MultiTenant.current_tenant_id.nil? && !record_tenant.nil? MultiTenant.with(record.public_send(partition_key)) { block.call } else block.call end } - around_update -> (record, block) { - if MultiTenant.current_tenant_id.nil? + around_update lambda { |record, block| + record_tenant = record.attribute_was(partition_key) + if MultiTenant.current_tenant_id.nil? && !record_tenant.nil? MultiTenant.with(record.public_send(partition_key)) { block.call } else block.call end } - around_destroy -> (record, block) { + around_destroy lambda { |record, block| if MultiTenant.current_tenant_id.nil? MultiTenant.with(record.public_send(partition_key)) { block.call } else @@ -121,20 +161,49 @@ def inherited(subclass) end end +# Below code block is executed on Model, Associations and CollectionProxy objects +# when ActiveRecord is loaded and decorates defined methods with MultiTenant.with function. +# Additionally, adds aliases for some operators. ActiveSupport.on_load(:active_record) do |base| base.extend MultiTenant::ModelExtensionsClassMethods + + # Ensure we have current_tenant_id in where clause when a cached ActiveRecord instance is being reloaded, + # or update_columns without callbacks is called + MultiTenant.wrap_methods(ActiveRecord::Base, 'self', :delete, :reload, :update_columns) + + # Any queuries fired for fetching a singular association have the correct current_tenant_id in WHERE clause + # reload is called anytime any record's association is accessed + MultiTenant.wrap_methods(ActiveRecord::Associations::Association, 'owner', :reload) + + # For collection associations, we need to wrap multiple methods in returned proxy so that + # any queries have the correct current_tenant_id in WHERE clause + ActiveRecord::Associations::CollectionProxy.alias_method \ + :equals_mt, :== # Hack to prevent syntax error due to invalid method name + ActiveRecord::Associations::CollectionProxy.alias_method \ + :append_mt, :<< # Hack to prevent syntax error due to invalid method name + MultiTenant.wrap_methods(ActiveRecord::Associations::CollectionProxy, '@association.owner', + :find, :last, :take, :build, :create, :create!, :replace, :delete_all, + :destroy_all, :delete, :destroy, :calculate, :pluck, :size, :empty?, :include?, :equals_mt, + :records, :append_mt, :find_nth_with_limit, :find_nth_from_last, :null_scope?, + :find_from_target?, :exec_queries) + ActiveRecord::Associations::CollectionProxy.alias_method :==, :equals_mt + ActiveRecord::Associations::CollectionProxy.alias_method :<<, :append_mt end -class ActiveRecord::Associations::Association - alias skip_statement_cache_orig skip_statement_cache? - def skip_statement_cache?(*scope) - return true if klass.respond_to?(:scoped_by_tenant?) && klass.scoped_by_tenant? +# skips statement caching for classes that is Multi-tenant or has a multi-tenant relation +module MultiTenant + module AssociationExtensions + def skip_statement_cache?(*scope) + return true if klass.respond_to?(:scoped_by_tenant?) && klass.scoped_by_tenant? + + if reflection.through_reflection + through_klass = reflection.through_reflection.klass + return true if through_klass.respond_to?(:scoped_by_tenant?) && through_klass.scoped_by_tenant? + end - if reflection.through_reflection - through_klass = reflection.through_reflection.klass - return true if through_klass.respond_to?(:scoped_by_tenant?) && through_klass.scoped_by_tenant? + super(*scope) end - - skip_statement_cache_orig(*scope) end end + +ActiveRecord::Associations::Association.prepend(MultiTenant::AssociationExtensions) diff --git a/lib/activerecord-multi-tenant/multi_tenant.rb b/lib/activerecord-multi-tenant/multi_tenant.rb index ec643977..e03008dc 100644 --- a/lib/activerecord-multi-tenant/multi_tenant.rb +++ b/lib/activerecord-multi-tenant/multi_tenant.rb @@ -1,53 +1,82 @@ -require 'request_store' +# frozen_string_literal: true + +require 'active_support/current_attributes' module MultiTenant - def self.tenant_klass_defined?(tenant_name) - !!tenant_name.to_s.classify.safe_constantize + class Current < ::ActiveSupport::CurrentAttributes + attribute :tenant + end + + def self.tenant_klass_defined?(tenant_name, options = {}) + class_name = if options[:class_name].present? + options[:class_name] + else + tenant_name.to_s.classify + end + !!class_name.safe_constantize end def self.partition_key(tenant_name) - "#{tenant_name.to_s}_id" + "#{tenant_name.to_s.underscore}_id" end + # rubocop:disable Style/ClassVars # In some cases we only have an ID - if defined we'll return the default tenant class in such cases - def self.default_tenant_class=(tenant_class); @@default_tenant_class = tenant_class; end - def self.default_tenant_class; @@default_tenant_class ||= nil; end + def self.default_tenant_class=(tenant_class) + @@default_tenant_class = tenant_class + end + + def self.default_tenant_class + @@default_tenant_class ||= nil + end # Write-only Mode - this only adds the tenant_id to new records, but doesn't # require its presence for SELECTs/UPDATEs/DELETEs - def self.enable_write_only_mode; @@enable_write_only_mode = true; end - def self.with_write_only_mode_enabled?; @@enable_write_only_mode ||= false; end + def self.enable_write_only_mode + @@enable_write_only_mode = true + end - # Workaroud to make "with_lock" work until https://github.com/citusdata/citus/issues/1236 is fixed - @@enable_with_lock_workaround = false - def self.enable_with_lock_workaround; @@enable_with_lock_workaround = true; end - def self.with_lock_workaround_enabled?; @@enable_with_lock_workaround; end + def self.with_write_only_mode_enabled? + @@enable_write_only_mode ||= false + end # Registry that maps table names to models (used by the query rewriter) - def self.register_multi_tenant_model(table_name, model_klass) - @@multi_tenant_models ||= {} - @@multi_tenant_models[table_name.to_s] = model_klass + def self.register_multi_tenant_model(model_klass) + @@multi_tenant_models ||= [] + @@multi_tenant_models.push(model_klass) + + remove_class_variable(:@@multi_tenant_model_table_names) if defined?(@@multi_tenant_model_table_names) end + def self.multi_tenant_model_for_table(table_name) - @@multi_tenant_models ||= {} - @@multi_tenant_models[table_name.to_s] + @@multi_tenant_models ||= [] + + unless defined?(@@multi_tenant_model_table_names) + @@multi_tenant_model_table_names = @@multi_tenant_models.map do |model| + [model.table_name, model] if model.table_name + end.compact.to_h + end + + @@multi_tenant_model_table_names[table_name.to_s] + # rubocop:enable Style/ClassVars end def self.multi_tenant_model_for_arel(arel) return nil unless arel.respond_to?(:ast) + if arel.ast.relation.is_a? Arel::Nodes::JoinSource - MultiTenant.multi_tenant_model_for_table(arel.ast.relation.left.table_name) + MultiTenant.multi_tenant_model_for_table(TableNode.table_name(arel.ast.relation.left)) else - MultiTenant.multi_tenant_model_for_table(arel.ast.relation.table_name) + MultiTenant.multi_tenant_model_for_table(TableNode.table_name(arel.ast.relation)) end end def self.current_tenant=(tenant) - RequestStore.store[:current_tenant] = tenant + Current.tenant = tenant end def self.current_tenant - RequestStore.store[:current_tenant] + Current.tenant end def self.current_tenant_id @@ -60,7 +89,7 @@ def self.current_tenant_is_id? def self.current_tenant_class if current_tenant_is_id? - MultiTenant.default_tenant_class || fail('Only have tenant id, and no default tenant class set') + MultiTenant.default_tenant_class || raise('Only have tenant id, and no default tenant class set') elsif current_tenant MultiTenant.current_tenant.class.name end @@ -69,35 +98,63 @@ def self.current_tenant_class def self.load_current_tenant! return MultiTenant.current_tenant if MultiTenant.current_tenant && !current_tenant_is_id? raise 'MultiTenant.current_tenant must be set to load' if MultiTenant.current_tenant.nil? - klass = MultiTenant.default_tenant_class || fail('Only have tenant id, and no default tenant class set') + + klass = MultiTenant.default_tenant_class || raise('Only have tenant id, and no default tenant class set') self.current_tenant = klass.find(MultiTenant.current_tenant_id) end def self.with(tenant, &block) - return block.call if self.current_tenant == tenant - old_tenant = self.current_tenant + return block.call if current_tenant == tenant + + old_tenant = current_tenant begin self.current_tenant = tenant - return block.call + block.call ensure self.current_tenant = old_tenant end end def self.without(&block) - return block.call if self.current_tenant.nil? - old_tenant = self.current_tenant + return block.call if current_tenant.nil? + + old_tenant = current_tenant begin self.current_tenant = nil - return block.call + block.call ensure self.current_tenant = old_tenant end end + # Wrap calls to any of `method_names` on an instance Class `klass` with MultiTenant.with + # when `'owner'` (evaluated in context of the klass instance) is a ActiveRecord model instance that is multi-tenant + # Instruments the methods provided with previously set Multitenant parameters + # TODO: Could not understand the use of owner here. Need to check + def self.wrap_methods(klass, owner, *method_names) + mod = Module.new + klass.prepend(mod) + + method_names.each do |method_name| + mod.module_eval <<-CODE, __FILE__, __LINE__ + 1 + def #{method_name}(...) + if MultiTenant.multi_tenant_model_for_table(#{owner}.class.table_name).present? && #{owner}.persisted? && MultiTenant.current_tenant_id.nil? && #{owner}.class.respond_to?(:partition_key) && #{owner}.attributes.include?(#{owner}.class.partition_key) + MultiTenant.with(#{owner}.public_send(#{owner}.class.partition_key)) { super } + else + super + end + end + CODE + end + end + # Preserve backward compatibility for people using .with_id singleton_class.send(:alias_method, :with_id, :with) + # This exception is raised when a there is an attempt to change tenant class TenantIsImmutable < StandardError end + + class MissingTenantError < StandardError + end end diff --git a/lib/activerecord-multi-tenant/persistence_extension.rb b/lib/activerecord-multi-tenant/persistence_extension.rb deleted file mode 100644 index e42e87c2..00000000 --- a/lib/activerecord-multi-tenant/persistence_extension.rb +++ /dev/null @@ -1,13 +0,0 @@ -module ActiveRecord - module Persistence - alias :delete_orig :delete - - def delete - if MultiTenant.multi_tenant_model_for_table(self.class.table_name).present? && persisted? && MultiTenant.current_tenant_id.nil? - MultiTenant.with(self.public_send(self.class.partition_key)) { delete_orig } - else - delete_orig - end - end - end -end diff --git a/lib/activerecord-multi-tenant/query_monitor.rb b/lib/activerecord-multi-tenant/query_monitor.rb index 097f9874..8af77b35 100644 --- a/lib/activerecord-multi-tenant/query_monitor.rb +++ b/lib/activerecord-multi-tenant/query_monitor.rb @@ -1,18 +1,36 @@ +# frozen_string_literal: true + # Add generic warning when queries fail and there is no tenant set +# To handle this case, a QueryMonitor hook is created and registered +# to sql.active_record. This hook will log a warning when a query fails +# This hook is executed after the query is executed. module MultiTenant + # rubocop:disable Style/ClassVars # Option to enable query monitor @@enable_query_monitor = false - def self.enable_query_monitor; @@enable_query_monitor = true; end - def self.query_monitor_enabled?; @@enable_query_monitor; end + def self.enable_query_monitor + @@enable_query_monitor = true + end + + def self.query_monitor_enabled? + @@enable_query_monitor + end + + # rubocop:enable Style/ClassVars + # QueryMonitor class to log a warning when a query fails and there is no tenant set + # start and finish methods are required to be register sql.active_record hook class QueryMonitor - def start(name, id, payload); end - def finish(name, id, payload) + def start(_name, _id, _payload) end + + def finish(_name, _id, payload) return unless MultiTenant.query_monitor_enabled? + return unless payload[:exception].present? && MultiTenant.current_tenant_id.nil? + Rails.logger.info 'WARNING: Tenant not present - make sure to add MultiTenant.with(tenant) { ... }' end end end - +# Actual code to register the hook. ActiveSupport::Notifications.subscribe('sql.active_record', MultiTenant::QueryMonitor.new) diff --git a/lib/activerecord-multi-tenant/query_rewriter.rb b/lib/activerecord-multi-tenant/query_rewriter.rb index f637752a..f516ffa1 100644 --- a/lib/activerecord-multi-tenant/query_rewriter.rb +++ b/lib/activerecord-multi-tenant/query_rewriter.rb @@ -1,6 +1,9 @@ +# frozen_string_literal: true + require 'active_record' -require_relative "./arel_visitors_depth_first.rb" unless Arel::Visitors.const_defined?(:DepthFirst) +require_relative 'arel_visitors_depth_first' unless Arel::Visitors.const_defined?(:DepthFirst) +# Iterates AST and adds tenant enforcement clauses to all relations module MultiTenant class Table attr_reader :arel_table @@ -9,9 +12,9 @@ def initialize(arel_table) @arel_table = arel_table end - def eql?(rhs) - self.class == rhs.class && - equality_fields.eql?(rhs.equality_fields) + def eql?(other) + self.class == other.class && + equality_fields.eql?(other.equality_fields) end def hash @@ -44,6 +47,7 @@ def discover_relations def visited_relation(relation) return unless @discovering + @known_relations << Table.new(relation) end @@ -56,9 +60,13 @@ def unhandled_relations end end - class ArelTenantVisitor < Arel::Visitors.const_defined?(:DepthFirst) ? Arel::Visitors::DepthFirst : ::MultiTenant::ArelVisitorsDepthFirst + class ArelTenantVisitor < if Arel::Visitors.const_defined?(:DepthFirst) + Arel::Visitors::DepthFirst + else + ::MultiTenant::ArelVisitorsDepthFirst + end def initialize(arel) - super(Proc.new {}) + super(proc {}) @statement_node_id = nil @contexts = [] @@ -68,59 +76,72 @@ def initialize(arel) attr_reader :contexts + # rubocop:disable Naming/MethodName def visit_Arel_Attributes_Attribute(*args) return if @current_context.nil? + super(*args) end - def visit_Arel_Nodes_Equality(o, *args) - if o.left.is_a?(Arel::Attributes::Attribute) - table_name = o.left.relation.table_name + def visit_Arel_Nodes_Equality(obj, *args) + if obj.left.is_a?(Arel::Attributes::Attribute) + table_name = MultiTenant::TableNode.table_name(obj.left.relation) model = MultiTenant.multi_tenant_model_for_table(table_name) - @current_context.visited_handled_relation(o.left.relation) if model.present? && o.left.name.to_s == model.partition_key.to_s + if model.present? && obj.left.name.to_s == model.partition_key.to_s + @current_context.visited_handled_relation(obj.left.relation) + end end - super(o, *args) + super(obj, *args) end - def visit_MultiTenant_TenantEnforcementClause(o, *) - @current_context.visited_handled_relation(o.tenant_attribute.relation) + def visit_MultiTenant_TenantEnforcementClause(obj, *) + @current_context.visited_handled_relation(obj.tenant_attribute.relation) end - def visit_MultiTenant_TenantJoinEnforcementClause(o, *) - @current_context.visited_handled_relation(o.tenant_attribute.relation) + def visit_MultiTenant_TenantJoinEnforcementClause(obj, *) + @current_context.visited_handled_relation(obj.tenant_attribute.relation) end - def visit_Arel_Table(o, _collector = nil) - @current_context.visited_relation(o) if tenant_relation?(o.table_name) + def visit_Arel_Table(obj, _collector = nil) + @current_context.visited_relation(obj) if tenant_relation?(MultiTenant::TableNode.table_name(obj)) end - alias :visit_Arel_Nodes_TableAlias :visit_Arel_Table - def visit_Arel_Nodes_SelectCore(o, *args) - nest_context(o) do + alias visit_Arel_Nodes_TableAlias visit_Arel_Table + + def visit_Arel_Nodes_SelectCore(obj, *_args) + nest_context(obj) do @current_context.discover_relations do - visit o.source + visit obj.source end - visit o.wheres - visit o.groups - visit o.windows - if defined?(o.having) - visit o.having + visit obj.wheres + visit obj.groups + visit obj.windows + if defined?(obj.having) + visit obj.having else - visit o.havings + visit obj.havings end end end - def visit_Arel_Nodes_OuterJoin(o, collector = nil) - nest_context(o) do + # rubocop:enable Naming/MethodName + + # rubocop:disable Naming/MethodName + def visit_Arel_Nodes_OuterJoin(obj, _collector = nil) + nest_context(obj) do @current_context.discover_relations do - visit o.left - visit o.right + visit obj.left + visit obj.right end end end - alias :visit_Arel_Nodes_FullOuterJoin :visit_Arel_Nodes_OuterJoin - alias :visit_Arel_Nodes_RightOuterJoin :visit_Arel_Nodes_OuterJoin + + # rubocop:enable Naming/MethodName + + alias visit_Arel_Nodes_FullOuterJoin visit_Arel_Nodes_OuterJoin + alias visit_Arel_Nodes_RightOuterJoin visit_Arel_Nodes_OuterJoin + + alias visit_ActiveModel_Attribute terminal private @@ -136,13 +157,16 @@ def dispatch DISPATCH end + # rubocop:disable Naming/AccessorMethodName def get_dispatch_cache dispatch end - def nest_context(o) + # rubocop:enable Naming/AccessorMethodName + + def nest_context(obj) old_context = @current_context - @current_context = Context.new(o) + @current_context = Context.new(obj) @contexts << @current_context yield @@ -153,25 +177,33 @@ def nest_context(o) class BaseTenantEnforcementClause < Arel::Nodes::Node attr_reader :tenant_attribute + def initialize(tenant_attribute) + super() @tenant_attribute = tenant_attribute - @tenant_model = MultiTenant.multi_tenant_model_for_table(tenant_attribute.relation.table_name) + @tenant_model = MultiTenant.multi_tenant_model_for_table( + MultiTenant::TableNode.table_name(tenant_attribute.relation) + ) end - def to_s; to_sql; end - def to_str; to_sql; end + def to_s + to_sql + end + + def to_str + to_sql + end def to_sql(*) collector = Arel::Collectors::SQLString.new collector = @tenant_model.connection.visitor.accept tenant_arel, collector collector.value end - - end class TenantEnforcementClause < BaseTenantEnforcementClause private + def tenant_arel if defined?(Arel::Nodes::Quoted) @tenant_attribute.eq(Arel::Nodes::Quoted.new(MultiTenant.current_tenant_id)) @@ -181,36 +213,39 @@ def tenant_arel end end - class TenantJoinEnforcementClause < BaseTenantEnforcementClause attr_reader :table_left + def initialize(tenant_attribute, table_left) super(tenant_attribute) @table_left = table_left - @model_left = MultiTenant.multi_tenant_model_for_table(table_left.table_name) + @model_left = MultiTenant.multi_tenant_model_for_table(MultiTenant::TableNode.table_name(table_left)) end private + def tenant_arel @tenant_attribute.eq(@table_left[@model_left.partition_key]) end end - module TenantValueVisitor - def visit_MultiTenant_TenantEnforcementClause(o, collector) - collector << o + # rubocop:disable Naming/MethodName + def visit_MultiTenant_TenantEnforcementClause(obj, collector) + collector << obj end - def visit_MultiTenant_TenantJoinEnforcementClause(o, collector) - collector << o + def visit_MultiTenant_TenantJoinEnforcementClause(obj, collector) + collector << obj end + + # rubocop:enable Naming/MethodName end module DatabaseStatements def join_to_update(update, *args) update = super(update, *args) - model = MultiTenant.multi_tenant_model_for_table(update.ast.relation.table_name) + model = MultiTenant.multi_tenant_model_for_table(MultiTenant::TableNode.table_name(update.ast.relation)) if model.present? && !MultiTenant.with_write_only_mode_enabled? && MultiTenant.current_tenant_id.present? update.where(MultiTenant::TenantEnforcementClause.new(model.arel_table[model.partition_key])) end @@ -219,7 +254,7 @@ def join_to_update(update, *args) def join_to_delete(delete, *args) delete = super(delete, *args) - model = MultiTenant.multi_tenant_model_for_table(delete.ast.left.table_name) + model = MultiTenant.multi_tenant_model_for_table(MultiTenant::TableNode.table_name(delete.ast.left)) if model.present? && !MultiTenant.with_write_only_mode_enabled? && MultiTenant.current_tenant_id.present? delete.where(MultiTenant::TenantEnforcementClause.new(model.arel_table[model.partition_key])) end @@ -249,63 +284,61 @@ def delete(arel, name = nil, binds = []) Arel::Visitors::ToSql.include(MultiTenant::TenantValueVisitor) -require 'active_record/relation' -module ActiveRecord - module QueryMethods - alias :build_arel_orig :build_arel - def build_arel(*args) - arel = build_arel_orig(*args) +module MultiTenant + module QueryMethodsExtensions + def build_arel(*) + arel = super - if !MultiTenant.with_write_only_mode_enabled? + unless MultiTenant.with_write_only_mode_enabled? visitor = MultiTenant::ArelTenantVisitor.new(arel) visitor.contexts.each do |context| node = context.arel_node context.unhandled_relations.each do |relation| - model = MultiTenant.multi_tenant_model_for_table(relation.arel_table.table_name) + model = MultiTenant.multi_tenant_model_for_table(MultiTenant::TableNode.table_name(relation.arel_table)) if MultiTenant.current_tenant_id enforcement_clause = MultiTenant::TenantEnforcementClause.new(relation.arel_table[model.partition_key]) case node - when Arel::Nodes::Join #Arel::Nodes::OuterJoin, Arel::Nodes::RightOuterJoin, Arel::Nodes::FullOuterJoin + when Arel::Nodes::Join # Arel::Nodes::OuterJoin, Arel::Nodes::RightOuterJoin, Arel::Nodes::FullOuterJoin node.right.expr = node.right.expr.and(enforcement_clause) when Arel::Nodes::SelectCore if node.wheres.empty? node.wheres = [enforcement_clause] + elsif node.wheres[0].is_a?(Arel::Nodes::And) + node.wheres[0].children << enforcement_clause else node.wheres[0] = enforcement_clause.and(node.wheres[0]) end else - raise "UnknownContext" + raise 'UnknownContext' end end - if node.is_a?(Arel::Nodes::SelectCore) || node.is_a?(Arel::Nodes::Join) - if node.is_a?Arel::Nodes::Join - node_list = [node] - else - node_list = node.source.right - end + next unless node.is_a?(Arel::Nodes::SelectCore) || node.is_a?(Arel::Nodes::Join) - node_list.select{ |n| n.is_a? Arel::Nodes::Join }.each do |node_join| - if (!node_join.right || - (ActiveRecord::VERSION::MAJOR == 5 && - !node_join.right.expr.right.is_a?(Arel::Attributes::Attribute))) - next - end + node_list = if node.is_a? Arel::Nodes::Join + [node] + else + node.source.right + end - relation_right, relation_left = relations_from_node_join(node_join) + node_list.select { |n| n.is_a? Arel::Nodes::Join }.each do |node_join| + next unless node_join.right - next unless relation_right && relation_left + relation_right, relation_left = relations_from_node_join(node_join) - model_right = MultiTenant.multi_tenant_model_for_table(relation_left.table_name) - model_left = MultiTenant.multi_tenant_model_for_table(relation_right.table_name) - if model_right && model_left - join_enforcement_clause = MultiTenant::TenantJoinEnforcementClause.new(relation_left[model_left.partition_key], relation_right) - node_join.right.expr = node_join.right.expr.and(join_enforcement_clause) - end - end + next unless relation_right && relation_left + + model_right = MultiTenant.multi_tenant_model_for_table(MultiTenant::TableNode.table_name(relation_left)) + model_left = MultiTenant.multi_tenant_model_for_table(MultiTenant::TableNode.table_name(relation_right)) + next unless model_right && model_left + + join_enforcement_clause = MultiTenant::TenantJoinEnforcementClause.new( + relation_right[model_right.partition_key], relation_left + ) + node_join.right.expr = node_join.right.expr.and(join_enforcement_clause) end end end @@ -315,28 +348,32 @@ def build_arel(*args) end private + def relations_from_node_join(node_join) - if ActiveRecord::VERSION::MAJOR == 5 || node_join.right.expr.is_a?(Arel::Nodes::Equality) + if node_join.right.expr.is_a?(Arel::Nodes::Equality) return node_join.right.expr.right.relation, node_join.right.expr.left.relation end - children = node_join.right.expr.children + children = [node_join.right.expr.children].flatten - tenant_applied = children.any?(MultiTenant::TenantEnforcementClause) || children.any?(MultiTenant::TenantJoinEnforcementClause) - if tenant_applied || children.empty? - return nil, nil + tenant_applied = children.any? do |c| + c.is_a?(MultiTenant::TenantEnforcementClause) || c.is_a?(MultiTenant::TenantJoinEnforcementClause) end + return nil, nil if tenant_applied || children.empty? - if children[0].right.respond_to?('relation') && children[0].left.respond_to?('relation') - return children[0].right.relation, children[0].left.relation + child = children.first.respond_to?(:children) ? children.first.children.first : children.first + if child.right.respond_to?(:relation) && child.left.respond_to?(:relation) + return child.right.relation, child.left.relation end - return nil, nil + [nil, nil] end end end -require 'active_record/base' +require 'active_record/relation' +ActiveRecord::QueryMethods.prepend(MultiTenant::QueryMethodsExtensions) + module MultiTenantFindBy def cached_find_by_statement(key, &block) return super unless respond_to?(:scoped_by_tenant?) && scoped_by_tenant? @@ -346,4 +383,6 @@ def cached_find_by_statement(key, &block) end end -ActiveRecord::Base.singleton_class.prepend(MultiTenantFindBy) +ActiveSupport.on_load(:active_record) do |base| + base.singleton_class.prepend(MultiTenantFindBy) +end diff --git a/lib/activerecord-multi-tenant/relation_extension.rb b/lib/activerecord-multi-tenant/relation_extension.rb new file mode 100644 index 00000000..c1d89002 --- /dev/null +++ b/lib/activerecord-multi-tenant/relation_extension.rb @@ -0,0 +1,64 @@ +# frozen_string_literal: true + +module Arel + module ActiveRecordRelationExtension + # Overrides the delete_all method to include tenant scoping + def delete_all + # Call the original delete_all method if the current tenant is identified by an ID + return super if MultiTenant.current_tenant_is_id? || MultiTenant.current_tenant.nil? + + stmt = Arel::DeleteManager.new.from(table) + stmt.wheres = [generate_in_condition_subquery] + + # Execute the delete statement using the connection and return the result + klass.connection.delete(stmt, "#{klass} Delete All").tap { reset } + end + + # Overrides the update_all method to include tenant scoping + def update_all(updates) + # Call the original update_all method if the current tenant is identified by an ID + return super if MultiTenant.current_tenant_is_id? || MultiTenant.current_tenant.nil? + + stmt = Arel::UpdateManager.new + stmt.table(table) + stmt.set Arel.sql(@klass.send(:sanitize_sql_for_assignment, updates)) + stmt.wheres = [generate_in_condition_subquery] + + klass.connection.update(stmt, "#{klass} Update All").tap { reset } + end + + private + + # The generate_in_condition_subquery method generates a subquery that selects + # records associated with the current tenant. + def generate_in_condition_subquery + # Get the tenant key and tenant ID based on the current tenant + tenant_key = MultiTenant.partition_key(MultiTenant.current_tenant_class) + tenant_id = MultiTenant.current_tenant_id + + # Build an Arel query + arel = eager_loading? ? apply_join_dependency.arel : build_arel + arel.source.left = table + + # If the tenant ID is present and the tenant key is a column in the model, + # add a condition to only include records where the tenant key equals the tenant ID + if tenant_id && klass.column_names.include?(tenant_key) + tenant_condition = table[tenant_key].eq(tenant_id) + unless arel.constraints.any? { |node| node.to_sql.include?(tenant_condition.to_sql) } + arel = arel.where(tenant_condition) + end + end + + # Clone the query, clear its projections, and set its projection to the primary key of the table + subquery = arel.clone + subquery.projections.clear + subquery = subquery.project(table[primary_key]) + + # Create an IN condition node with the primary key of the table and the subquery + Arel::Nodes::In.new(table[primary_key], subquery.ast) + end + end +end + +# Patch ActiveRecord::Relation with the extension module +ActiveRecord::Relation.prepend(Arel::ActiveRecordRelationExtension) diff --git a/lib/activerecord-multi-tenant/sidekiq.rb b/lib/activerecord-multi-tenant/sidekiq.rb index 2e39c9d5..0bca5257 100644 --- a/lib/activerecord-multi-tenant/sidekiq.rb +++ b/lib/activerecord-multi-tenant/sidekiq.rb @@ -1,14 +1,19 @@ +# frozen_string_literal: true + require 'sidekiq/client' +# Adds methods to handle tenant information both in the client and server. module Sidekiq::Middleware::MultiTenant # Get the current tenant and store in the message to be sent to Sidekiq. class Client - def call(worker_class, msg, queue, redis_pool) - msg['multi_tenant'] ||= - { - 'class' => MultiTenant.current_tenant_class, - 'id' => MultiTenant.current_tenant_id - } if MultiTenant.current_tenant.present? + def call(_worker_class, msg, _queue, _redis_pool) + if MultiTenant.current_tenant.present? + msg['multi_tenant'] ||= + { + 'class' => MultiTenant.current_tenant_class, + 'id' => MultiTenant.current_tenant_id + } + end yield end @@ -16,12 +21,14 @@ def call(worker_class, msg, queue, redis_pool) # Pull the tenant out and run the current thread with it. class Server - def call(worker_class, msg, queue) - if msg.has_key?('multi_tenant') - tenant = msg['multi_tenant']['class'].constantize.find(msg['multi_tenant']['id']) - MultiTenant.with(tenant) do - yield + def call(_worker_class, msg, _queue, &block) + if msg.key?('multi_tenant') + tenant = begin + msg['multi_tenant']['class'].constantize.find(msg['multi_tenant']['id']) + rescue ActiveRecord::RecordNotFound + msg['multi_tenant']['id'] end + MultiTenant.with(tenant, &block) else yield end @@ -29,26 +36,52 @@ def call(worker_class, msg, queue) end end +# Configure Sidekiq to use the multi-tenant client and server middleware to add (client/server)/process(server) +# tenant information. +Sidekiq.configure_server do |config| + config.server_middleware do |chain| + chain.add Sidekiq::Middleware::MultiTenant::Server + end + config.client_middleware do |chain| + chain.add Sidekiq::Middleware::MultiTenant::Client + end +end + +Sidekiq.configure_client do |config| + config.client_middleware do |chain| + chain.add Sidekiq::Middleware::MultiTenant::Client + end +end + +# Bulk push support for Sidekiq while setting multi-tenant information. +# This is a copy of the Sidekiq::Client#push_bulk method with the addition of +# setting the multi-tenant information for each job. module Sidekiq class Client + # Allows the caller to enqueue multiple Sidekiq jobs with + # tenant information in a single call. It ensures that each job is processed + # within the correct tenant context and returns an array of job IDs for the enqueued jobs def push_bulk_with_tenants(items) - job = items['jobs'].first - return [] unless job # no jobs to push - raise ArgumentError, "Bulk arguments must be an Array of Hashes: [{ 'args' => [1], 'tenant_id' => 1 }, ...]" if !job.is_a?(Hash) + first_job = items['jobs'].first + return [] unless first_job # no jobs to push + unless first_job.is_a?(Hash) + raise ArgumentError, "Bulk arguments must be an Array of Hashes: [{ 'args' => [1], 'tenant_id' => 1 }, ...]" + end normed = normalize_item(items.except('jobs').merge('args' => [])) payloads = items['jobs'].map do |job| MultiTenant.with(job['tenant_id']) do copy = normed.merge('args' => job['args'], 'jid' => SecureRandom.hex(12), 'enqueued_at' => Time.now.to_f) result = process_single(items['class'], copy) - result ? result : nil + result || nil end end.compact - raw_push(payloads) if !payloads.empty? + raw_push(payloads) unless payloads.empty? payloads.collect { |payload| payload['jid'] } end + # Enabling the push_bulk_with_tenants method to be called directly on the Sidekiq::Client class class << self def push_bulk_with_tenants(items) new.push_bulk_with_tenants(items) diff --git a/lib/activerecord-multi-tenant/table_node.rb b/lib/activerecord-multi-tenant/table_node.rb new file mode 100644 index 00000000..9431b06c --- /dev/null +++ b/lib/activerecord-multi-tenant/table_node.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module MultiTenant + module TableNode + # Return table name + def self.table_name(node) + # NOTE: Arel::Nodes::Table#table_name is removed in Rails 7.1 + if node.is_a?(Arel::Nodes::TableAlias) + node.table_name + else + node.name + end + end + end +end diff --git a/lib/activerecord-multi-tenant/version.rb b/lib/activerecord-multi-tenant/version.rb index eb2deee5..20299e68 100644 --- a/lib/activerecord-multi-tenant/version.rb +++ b/lib/activerecord-multi-tenant/version.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module MultiTenant - VERSION = '1.1.1' + VERSION = '2.4.0' end diff --git a/lib/activerecord-multi-tenant/with_lock.rb b/lib/activerecord-multi-tenant/with_lock.rb deleted file mode 100644 index 14521017..00000000 --- a/lib/activerecord-multi-tenant/with_lock.rb +++ /dev/null @@ -1,15 +0,0 @@ -# Workaround for https://github.com/citusdata/citus/issues/1236 -# "SELECT ... FOR UPDATE is not supported for router-plannable queries" - -class ActiveRecord::Base - alias :lock_orig :lock! - def lock!(lock = true) - if lock && persisted? && self.class.respond_to?(:scoped_by_tenant?) && MultiTenant.current_tenant_id && MultiTenant.with_lock_workaround_enabled? - self.class.unscoped.where(id: id).update_all(id: id) # No-op UPDATE that locks the row - reload # This is just to act similar to the default ActiveRecord approach, in case someone relies on the reload - self - else - lock_orig(lock) - end - end -end diff --git a/lib/activerecord_multi_tenant.rb b/lib/activerecord_multi_tenant.rb new file mode 100644 index 00000000..7c4fc30b --- /dev/null +++ b/lib/activerecord_multi_tenant.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +require_relative 'activerecord-multi-tenant/controller_extensions' if Object.const_defined?(:ActionController) +require_relative 'activerecord-multi-tenant/copy_from_client' +require_relative 'activerecord-multi-tenant/fast_truncate' +require_relative 'activerecord-multi-tenant/migrations' +require_relative 'activerecord-multi-tenant/model_extensions' +require_relative 'activerecord-multi-tenant/multi_tenant' +require_relative 'activerecord-multi-tenant/query_rewriter' +require_relative 'activerecord-multi-tenant/query_monitor' +require_relative 'activerecord-multi-tenant/table_node' +require_relative 'activerecord-multi-tenant/version' +require_relative 'activerecord-multi-tenant/habtm' +require_relative 'activerecord-multi-tenant/relation_extension' diff --git a/spec/activerecord-multi-tenant/associations_spec.rb b/spec/activerecord-multi-tenant/associations_spec.rb new file mode 100644 index 00000000..d74cd24c --- /dev/null +++ b/spec/activerecord-multi-tenant/associations_spec.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe MultiTenant, 'Association methods' do + let(:account1) { Account.create! name: 'test1' } + let(:account2) { Account.create! name: 'test2' } + let(:project1) { Project.create! name: 'something1', account: account1 } + let(:project2) { Project.create! name: 'something2', account: account2, id: project1.id } + + let(:task1) { Task.create! name: 'task1', project: project1, account: account1 } + let(:task2) { Task.create! name: 'task2', project: project2, account: account2, id: task1.id } + let(:manager1) { Manager.create! name: 'manager1', account: account1, tasks: [task1] } + let(:project3) { Project.create! name: 'something3', account: account1, managers: [manager1] } + + context 'include the tenant_id in queries and' do + it 'creates a task with correct account_id' do + expect(project2.tasks.create(name: 'task3').account_id).to eq(account2.id) + end + it 'return correct account_id' do + expect(task1.project.account_id).to_not eq(task2.project.account_id) # belongs_to + expect(project2.tasks.count).to eq(1) + expect(project2.tasks.first.account_id).to eq(account2.id) # has_many + end + + it 'check has_many_belongs_to' do + MultiTenant.with(account1) do + expect(manager1.tasks.first.account_id).to eq(task1.account_id) # has_many + end + end + + it 'check has_many_belongs_to without tenant in the intermediate table' do + MultiTenant.with(account1) do + expect(manager1.tasks.first.account_id).to eq(task1.account_id) # has_many + end + end + + it 'check has_many_belongs_to tenant_enabled false' do + MultiTenant.with(account1) do + expect(project3.managers.first.id).to eq(manager1.id) # has_many + end + end + end +end diff --git a/spec/activerecord-multi-tenant/controller_extensions_spec.rb b/spec/activerecord-multi-tenant/controller_extensions_spec.rb index bceaa88c..6e663586 100644 --- a/spec/activerecord-multi-tenant/controller_extensions_spec.rb +++ b/spec/activerecord-multi-tenant/controller_extensions_spec.rb @@ -1,6 +1,8 @@ +# frozen_string_literal: true + require 'spec_helper' -describe "Controller Extensions", type: :controller do +describe 'Controller Extensions', type: :controller do class Account attr_accessor :name end @@ -31,7 +33,6 @@ def index end end - class APIApplicationController < ActionController::API include Rails.application.routes.url_helpers set_current_tenant_through_filter diff --git a/spec/activerecord-multi-tenant/fast_truncate_spec.rb b/spec/activerecord-multi-tenant/fast_truncate_spec.rb index 634b958e..f665241b 100644 --- a/spec/activerecord-multi-tenant/fast_truncate_spec.rb +++ b/spec/activerecord-multi-tenant/fast_truncate_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe MultiTenant::FastTruncate do @@ -5,19 +7,19 @@ MultiTenant::FastTruncate.run end - it "truncates tables that have exactly one row inserted" do + it 'truncates tables that have exactly one row inserted' do Account.create! name: 'foo' - expect { + expect do MultiTenant::FastTruncate.run - }.to change { Account.count }.from(1).to(0) + end.to change { Account.count }.from(1).to(0) end - it "truncates tables that have more than one row inserted" do + it 'truncates tables that have more than one row inserted' do Account.create! name: 'foo' Account.create! name: 'bar' - expect { + expect do MultiTenant::FastTruncate.run - }.to change { Account.count }.from(2).to(0) + end.to change { Account.count }.from(2).to(0) end end diff --git a/spec/activerecord-multi-tenant/model_extensions_spec.rb b/spec/activerecord-multi-tenant/model_extensions_spec.rb index 00e3bf33..a49af989 100644 --- a/spec/activerecord-multi-tenant/model_extensions_spec.rb +++ b/spec/activerecord-multi-tenant/model_extensions_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe MultiTenant do @@ -10,11 +12,11 @@ end describe 'is_scoped_as_tenant should return the correct value when true' do - it {expect(Project.respond_to?(:scoped_by_tenant?)).to eq(true)} + it { expect(Project.respond_to?(:scoped_by_tenant?)).to eq(true) } end describe 'is_scoped_as_tenant should return the correct value when false' do - it {expect(UnscopedModel.respond_to?(:scoped_by_tenant?)).to eq(false)} + it { expect(UnscopedModel.respond_to?(:scoped_by_tenant?)).to eq(false) } end context 'immutability' do @@ -43,17 +45,17 @@ @account = Account.create! name: 'foo' MultiTenant.current_tenant = @account end - it {expect(Project.new.account_id).to eq(@account.id)} + it { expect(Project.new.account_id).to eq(@account.id) } it 'should handle partial selects' do project = Project.create! - expect{project = Project.select(:name).find(project.id)}.not_to raise_error + expect { project = Project.select(:name).find(project.id) }.not_to raise_error expect(project.account_id).to eq(@account.id) end end describe 'Handles custom partition_key on tenant model' do before do - @account = Account.create! name: 'foo' + @account = Account.create! name: 'foo' MultiTenant.current_tenant = @account @custom_partition_key_task = CustomPartitionKeyTask.create! name: 'foo' end @@ -70,6 +72,72 @@ it { expect(@partition_key_not_model_task.non_model_id).to be 77 } end + describe 'Tenant model with a nonstandard class name' do + let(:account_klass) do + Class.new(ActiveRecord::Base) do + self.table_name = 'account' + + def self.name + 'UserAccount' + end + + multi_tenant(:account) + end + end + it 'does not register the tenant model' do + expect(MultiTenant).not_to receive(:register_multi_tenant_model) + account_klass + end + end + + describe 'Changes table_name after multi_tenant called' do + before do + account_klass.has_many(:posts, anonymous_class: post_klass) + post_klass.belongs_to(:account, anonymous_class: account_klass) + + @account1 = account_klass.create! name: 'foo' + @account2 = account_klass.create! name: 'bar' + + @post1 = @account1.posts.create! name: 'foobar' + @post2 = @account2.posts.create! name: 'baz' + + MultiTenant.current_tenant = @account1 + @posts = post_klass.all + end + + let(:account_klass) do + Class.new(Account) do + def self.name + 'Account' + end + end + end + + let(:post_klass) do + Class.new(ActiveRecord::Base) do + self.table_name = 'unknown' + + multi_tenant(:account) + + self.table_name = 'posts' + + def self.name + 'Post' + end + end + end + + it { expect(@posts.length).to eq(1) } + it { expect(@posts).to eq([@post1]) } + end + + describe 'inspect method filters senstive column values' do + it 'filters senstive value' do + account = Account.new(name: 'foo', password: 'baz') + expect(account.inspect).to eq '#' + end + end + # Scoping models describe 'Project.all should be scoped to the current tenant if set' do before do @@ -112,16 +180,16 @@ end end - describe "It should be possible to use aliased associations" do + describe 'It should be possible to use aliased associations' do before do @account = Account.create! name: 'baz' MultiTenant.current_tenant = @account end - it { expect(AliasedTask.create(:name => 'foo', :project_alias => @project2).valid?).to eq(true) } + it { expect(AliasedTask.create(name: 'foo', project_alias: @project2).valid?).to eq(true) } end - describe "It should be possible to use associations with partition_key from polymorphic" do + describe 'It should be possible to use associations with partition_key from polymorphic' do before do @account = Account.create!(name: 'foo') MultiTenant.current_tenant = @account @@ -144,6 +212,22 @@ end end + it 'handles belongs_to with optional: true' do + record = OptionalSubTask.create(sub_task_id: sub_task.id) + expect(record.reload.sub_task).to eq(sub_task) + expect(record.account_id).to eq(nil) + end + + it 'handles changing tenant from nil to a value' do + record = OptionalSubTask.create(sub_task_id: sub_task.id) + expect(record.reload.sub_task).to eq(sub_task) + expect(record.account_id).to eq(nil) + + record.account = account + record.save! + expect(record.reload.account_id).to eq(account.id) + end + it 'handles has_many through' do MultiTenant.with(account) do expect(project.sub_tasks).to eq [sub_task] @@ -162,7 +246,7 @@ MultiTenant.with(account) do sub_task manager - expect(Project.eager_load([{manager: :project}, {tasks: :project}]).first).to eq project + expect(Project.eager_load([{ manager: :project }, { tasks: :project }]).first).to eq project end end end @@ -173,7 +257,8 @@ it 'rewrites sub-selects correctly' do MultiTenant.with(account) do - expect(Project.where(id: Project.where(id: project.id)).where(id: Project.where(id: project.id)).first).to eq project + expect(Project.where(id: Project.where(id: project.id)) + .where(id: Project.where(id: project.id)).first).to eq project end end end @@ -202,70 +287,132 @@ end describe 'non-STI Subclass of abstract Multi Tenant Model' do - let(:tenant_id_1) { 42 } - let(:tenant_id_2) { 314158 } + let(:tenant_id1) { 42 } + let(:tenant_id2) { 314_158 } let(:name) { 'fooname' } - let(:subclass_task_1) do - MultiTenant.with(tenant_id_1) { SubclassTask.create! name: name } + let(:subclass_task1) do + MultiTenant.with(tenant_id1) { SubclassTask.create! name: name } end - let(:subclass_task_2) do - MultiTenant.with(tenant_id_2) { SubclassTask.create! name: name } + let(:subclass_task2) do + MultiTenant.with(tenant_id2) { SubclassTask.create! name: name } end before do - subclass_task_1 - subclass_task_2 + subclass_task1 + subclass_task2 end it 'injects tenant_id on create' do - expect(subclass_task_1.non_model_id).to be tenant_id_1 - expect(subclass_task_2.non_model_id).to be tenant_id_2 + expect(subclass_task1.non_model_id).to be tenant_id1 + expect(subclass_task2.non_model_id).to be tenant_id2 end it 'rewrites query' do - MultiTenant.with(tenant_id_1) do + MultiTenant.with(tenant_id1) do expect(SubclassTask.where(name: name).count).to eq 1 - expect(SubclassTask.where(name: name).first).to eq subclass_task_1 + expect(SubclassTask.where(name: name).first).to eq subclass_task1 end - MultiTenant.with(tenant_id_2) do + MultiTenant.with(tenant_id2) do expect(SubclassTask.where(name: name).count).to eq 1 - expect(SubclassTask.where(name: name).first).to eq subclass_task_2 + expect(SubclassTask.where(name: name).first).to eq subclass_task2 + end + end + end + + # Joins + describe 'joins for models' do + context 'for models with where condition in associations' do + let(:account) { Account.create!(name: 'Account 1') } + + it 'should add tenant condition to the queries when tenant is set' do + expected_join_sql = <<-SQL.strip + SELECT "comments".*#{' '} + FROM "comments"#{' '} + INNER JOIN "tasks" ON "tasks"."id" = "comments"."commentable_id"#{' '} + AND "comments"."commentable_type" = 'Task' AND "tasks"."account_id" = 1#{' '} + WHERE "comments"."account_id" = 1 + SQL + + MultiTenant.with(account) do + expect(format_sql(Comment.joins(:task).to_sql)).to eq(format_sql(expected_join_sql)) + end + end + + it 'should add tenant condition to the queries when tenant is not set' do + MultiTenant.without do + expected_join_sql = <<-SQL.strip + SELECT "comments".*#{' '} + FROM "comments"#{' '} + INNER JOIN "tasks" ON "tasks"."id" = "comments"."commentable_id"#{' '} + AND "comments"."commentable_type" = 'Task' AND "comments"."account_id" = "tasks"."account_id" + SQL + expect(format_sql(Comment.joins(:task).to_sql)).to eq(format_sql(expected_join_sql)) + end + end + end + + context 'for models with default associations' do + let(:account) { Account.create!(name: 'Account 1') } + + it 'should add tenant condition to the queries when tenant is set' do + expected_join_sql = <<-SQL.strip + SELECT "projects".*#{' '} + FROM "projects"#{' '} + INNER JOIN "tasks" ON "tasks"."project_id" = "projects"."id"#{' '} + AND "tasks"."account_id" = 1#{' '} + WHERE "projects"."account_id" = 1 + SQL + + MultiTenant.with(account) do + expect(format_sql(Project.joins(:tasks).to_sql)).to eq(format_sql(expected_join_sql)) + end + end + + it 'should add tenant condition to the queries when tenant is not set' do + MultiTenant.without do + expected_join_sql = <<-SQL.strip + SELECT "projects".* + FROM "projects" + INNER JOIN "tasks" ON "tasks"."project_id" = "projects"."id" + AND "projects"."account_id" = "tasks"."account_id" + SQL + expect(format_sql(Project.joins(:tasks).to_sql)).to eq(format_sql(expected_join_sql)) + end end end end # ::with - describe "::with" do - it "should set current_tenant to the specified tenant inside the block" do - @account = Account.create!(:name => 'baz') + describe '::with' do + it 'should set current_tenant to the specified tenant inside the block' do + @account = Account.create!(name: 'baz') MultiTenant.with(@account) do expect(MultiTenant.current_tenant).to eq(@account) end end - it "should reset current_tenant to the previous tenant once exiting the block" do - @account1 = Account.create!(:name => 'foo') - @account2 = Account.create!(:name => 'bar') + it 'should reset current_tenant to the previous tenant once exiting the block' do + @account1 = Account.create!(name: 'foo') + @account2 = Account.create!(name: 'bar') MultiTenant.current_tenant = @account1 MultiTenant.with @account2 do - end expect(MultiTenant.current_tenant).to eq(@account1) end - it "should return the value of the block" do - @account1 = Account.create!(:name => 'foo') - @account2 = Account.create!(:name => 'bar') + it 'should return the value of the block' do + @account1 = Account.create!(name: 'foo') + @account2 = Account.create!(name: 'bar') MultiTenant.current_tenant = @account1 value = MultiTenant.with @account2 do - "something" + 'something' end - expect(value).to eq "something" + expect(value).to eq 'something' end it 'supports reload inside the block' do @@ -280,9 +427,9 @@ end # ::without - describe "::without" do - it "should unset current_tenant inside the block" do - @account = Account.create!(:name => 'baz') + describe '::without' do + it 'should unset current_tenant inside the block' do + @account = Account.create!(name: 'baz') MultiTenant.current_tenant = @account MultiTenant.without do @@ -290,39 +437,25 @@ end end - it "should reset current_tenant to the previous tenant once exiting the block" do - @account1 = Account.create!(:name => 'foo') + it 'should reset current_tenant to the previous tenant once exiting the block' do + @account1 = Account.create!(name: 'foo') MultiTenant.current_tenant = @account1 MultiTenant.without do - end expect(MultiTenant.current_tenant).to eq(@account1) end - it "should return the value of the block" do - @account1 = Account.create!(:name => 'foo') + it 'should return the value of the block' do + @account1 = Account.create!(name: 'foo') MultiTenant.current_tenant = @account1 value = MultiTenant.without do - "something" + 'something' end - expect(value).to eq "something" - end - end - - describe '.with_lock' do - it 'supports with_lock blocks inside the block' do - @account = Account.create!(name: 'foo') - - MultiTenant.with @account do - project = @account.projects.create!(name: 'project') - project.with_lock do - expect(project.name).to eq 'project' - end - end + expect(value).to eq 'something' end end @@ -347,10 +480,20 @@ end end - it "applies the team_id conditions in the where clause" do - expected_sql = <<-sql - SELECT "sub_tasks".* FROM "sub_tasks" INNER JOIN "tasks" ON "sub_tasks"."task_id" = "tasks"."id" AND "sub_tasks"."account_id" = "tasks"."account_id" WHERE "tasks"."account_id" = 1 AND "sub_tasks"."account_id" = 1 AND "tasks"."project_id" = 1 - sql + it 'applies the team_id conditions in the where clause' do + option1 = <<-SQL.strip + SELECT "sub_tasks".*#{' '} + FROM "sub_tasks"#{' '} + INNER JOIN "tasks" ON "sub_tasks"."task_id" = "tasks"."id" AND "tasks"."account_id" = "sub_tasks"."account_id"#{' '} + WHERE "tasks"."project_id" = 1 AND "sub_tasks"."account_id" = 1 AND "tasks"."account_id" = 1 + SQL + option2 = <<-SQL.strip + SELECT "sub_tasks".*#{' '} + FROM "sub_tasks"#{' '} + INNER JOIN "tasks" ON "sub_tasks"."task_id" = "tasks"."id"#{' '} + AND "tasks"."account_id" = "sub_tasks"."account_id"#{' '} + WHERE "sub_tasks"."account_id" = 1 AND "tasks"."project_id" = 1 AND "tasks"."account_id" = 1 + SQL account1 = Account.create! name: 'Account 1' @@ -358,24 +501,39 @@ project1 = Project.create! name: 'Project 1' task1 = Task.create! name: 'Task 1', project: project1 subtask1 = SubTask.create! task: task1 - expect(project1.sub_tasks.to_sql).to eq(expected_sql.strip) + expect(format_sql(project1.sub_tasks.to_sql)) + .to eq(format_sql(option1)).or(eq(format_sql(option2))) expect(project1.sub_tasks).to include(subtask1) end MultiTenant.without do - expected_sql = <<-sql - SELECT "sub_tasks".* FROM "sub_tasks" INNER JOIN "tasks" ON "sub_tasks"."task_id" = "tasks"."id" AND "sub_tasks"."account_id" = "tasks"."account_id" WHERE "tasks"."project_id" = 1 - sql + expected_sql = <<-SQL + SELECT "sub_tasks".*#{' '} + FROM "sub_tasks"#{' '} + INNER JOIN "tasks" ON "sub_tasks"."task_id" = "tasks"."id"#{' '} + AND "tasks"."account_id" = "sub_tasks"."account_id"#{' '} + WHERE "tasks"."project_id" = 1 + SQL project = Project.first - expect(project.sub_tasks.to_sql).to eq(expected_sql.strip) + expect(format_sql(project.sub_tasks.to_sql)).to eq(format_sql(expected_sql.strip)) end end - it "tests joins between distributed and reference table" do - expected_sql = <<-sql - SELECT "categories".* FROM "categories" INNER JOIN "project_categories" ON "categories"."id" = "project_categories"."category_id" WHERE "project_categories"."account_id" = 1 AND "project_categories"."project_id" = 1 - sql + it 'tests joins between distributed and reference table' do + option1 = <<-SQL.strip + SELECT "categories".*#{' '} + FROM "categories"#{' '} + INNER JOIN "project_categories" ON "categories"."id" = "project_categories"."category_id"#{' '} + WHERE "project_categories"."project_id" = 1 AND "project_categories"."account_id" = 1 + SQL + option2 = <<-SQL.strip + SELECT "categories".*#{' '} + FROM "categories"#{' '} + INNER JOIN "project_categories" ON "categories"."id" = "project_categories"."category_id"#{' '} + WHERE "project_categories"."account_id" = 1 AND "project_categories"."project_id" = 1 + SQL + account1 = Account.create! name: 'Account 1' category1 = Category.create! name: 'Category 1' @@ -383,50 +541,72 @@ project1 = Project.create! name: 'Project 1' projectcategory = ProjectCategory.create! name: 'project cat 1', project: project1, category: category1 - expect(project1.categories.to_sql).to eq(expected_sql.strip) + expect(format_sql(project1.categories.to_sql)) + .to eq(format_sql(option1)).or(eq(format_sql(option2))) expect(project1.categories).to include(category1) expect(project1.project_categories).to include(projectcategory) end MultiTenant.without do - expected_sql = <<-sql - SELECT "categories".* FROM "categories" INNER JOIN "project_categories" ON "categories"."id" = "project_categories"."category_id" WHERE "project_categories"."project_id" = 1 - sql + expected_sql = <<-SQL + SELECT "categories".*#{' '} + FROM "categories"#{' '} + INNER JOIN "project_categories" ON "categories"."id" = "project_categories"."category_id"#{' '} + WHERE "project_categories"."project_id" = 1 + SQL project = Project.first - expect(project.categories.to_sql).to eq(expected_sql.strip) + expect(format_sql(project.categories.to_sql)) + .to eq(format_sql(expected_sql.strip)) expect(project.categories).to include(category1) - expected_sql = <<-sql - SELECT "projects".* FROM "projects" INNER JOIN "project_categories" ON "project_categories"."project_id" = "projects"."id" AND "project_categories"."account_id" = "projects"."account_id" INNER JOIN "categories" ON "categories"."id" = "project_categories"."category_id" WHERE "projects"."account_id" = 1 - sql + expected_sql = <<-SQL + SELECT "projects".* FROM "projects"#{' '} + INNER JOIN "project_categories" ON "project_categories"."project_id" = "projects"."id"#{' '} + AND "projects"."account_id" = "project_categories"."account_id"#{' '} + INNER JOIN "categories" ON "categories"."id" = "project_categories"."category_id"#{' '} + WHERE "projects"."account_id" = 1 + SQL - expect(Project.where(account_id: 1).joins(:categories).to_sql).to eq(expected_sql.strip) + expect(format_sql(Project.where(account_id: 1).joins(:categories).to_sql)) + .to eq(format_sql(expected_sql.strip)) project = Project.where(account_id: 1).joins(:categories).first expect(project.categories).to include(category1) end end - - it "test eager_load" do + it 'test eager_load' do account1 = Account.create! name: 'Account 1' category1 = Category.create! name: 'Category 1' - expected_sql = if uses_prepared_statements? && (ActiveRecord::VERSION::MAJOR == 5 || (ActiveRecord::VERSION::MAJOR == 6 && ActiveRecord::VERSION::MINOR >= 1)) - <<-sql - SELECT "projects"."id" AS t0_r0, "projects"."account_id" AS t0_r1, "projects"."name" AS t0_r2, "categories"."id" AS t1_r0, "categories"."name" AS t1_r1 FROM "projects" LEFT OUTER JOIN "project_categories" ON "project_categories"."project_id" = "projects"."id" AND "project_categories"."account_id" = 1 AND "projects"."account_id" = 1 LEFT OUTER JOIN "categories" ON "categories"."id" = "project_categories"."category_id" AND "project_categories"."account_id" = 1 WHERE "projects"."account_id" = 1 - sql - else - <<-sql - SELECT "projects"."id" AS t0_r0, "projects"."account_id" AS t0_r1, "projects"."name" AS t0_r2, "categories"."id" AS t1_r0, "categories"."name" AS t1_r1 FROM "projects" LEFT OUTER JOIN "project_categories" ON "project_categories"."account_id" = 1 AND "project_categories"."project_id" = "projects"."id" AND "projects"."account_id" = 1 LEFT OUTER JOIN "categories" ON "categories"."id" = "project_categories"."category_id" AND "project_categories"."account_id" = 1 WHERE "projects"."account_id" = 1 - sql - end + option1 = <<-SQL.strip + SELECT "projects"."id" AS t0_r0, "projects"."account_id" AS t0_r1, "projects"."name" AS t0_r2, + "categories"."id" AS t1_r0, "categories"."name" AS t1_r1 + FROM "projects" + LEFT OUTER JOIN "project_categories" + ON "project_categories"."project_id" = "projects"."id" AND "project_categories"."account_id" = 1 + AND "projects"."account_id" = 1#{' '} + LEFT OUTER JOIN "categories" ON "categories"."id" = "project_categories"."category_id" + AND "project_categories"."account_id" = 1 + WHERE "projects"."account_id" = 1 + SQL + option2 = <<-SQL.strip + SELECT "projects"."id" AS t0_r0, "projects"."account_id" AS t0_r1, "projects"."name" AS t0_r2,#{' '} + "categories"."id" AS t1_r0, "categories"."name" AS t1_r1#{' '} + FROM "projects"#{' '} + LEFT OUTER JOIN "project_categories"#{' '} + ON "project_categories"."account_id" = 1#{' '} + AND "project_categories"."project_id" = "projects"."id" AND "projects"."account_id" = 1#{' '} + LEFT OUTER JOIN "categories" ON "categories"."id" = "project_categories"."category_id"#{' '} + AND "project_categories"."account_id" = 1 WHERE "projects"."account_id" = 1 + SQL MultiTenant.with(account1) do project1 = Project.create! name: 'Project 1' projectcategory = ProjectCategory.create! name: 'project cat 1', project: project1, category: category1 - expect(Project.eager_load(:categories).to_sql).to eq(expected_sql.strip) + expect(format_sql(Project.eager_load(:categories).to_sql)) + .to eq(format_sql(option1)).or(eq(format_sql(option2))) project = Project.eager_load(:categories).first expect(project.categories).to include(category1) @@ -434,88 +614,120 @@ end MultiTenant.without do - expected_sql = <<-sql - SELECT "projects"."id" AS t0_r0, "projects"."account_id" AS t0_r1, "projects"."name" AS t0_r2, "categories"."id" AS t1_r0, "categories"."name" AS t1_r1 FROM "projects" LEFT OUTER JOIN "project_categories" ON "project_categories"."project_id" = "projects"."id" AND "project_categories"."account_id" = "projects"."account_id" LEFT OUTER JOIN "categories" ON "categories"."id" = "project_categories"."category_id" WHERE "projects"."account_id" = 1 - sql - - expect(Project.where(account_id: 1).eager_load(:categories).to_sql).to eq(expected_sql.strip) + expected_sql = <<-SQL + SELECT "projects"."id" AS t0_r0, "projects"."account_id" AS t0_r1, "projects"."name" AS t0_r2,#{' '} + "categories"."id" AS t1_r0, "categories"."name" AS t1_r1#{' '} + FROM "projects" LEFT OUTER JOIN "project_categories"#{' '} + ON "project_categories"."project_id" = "projects"."id" AND "projects"."account_id" = "project_categories"."account_id"#{' '} + LEFT OUTER JOIN "categories"#{' '} + ON "categories"."id" = "project_categories"."category_id"#{' '} + WHERE "projects"."account_id" = 1 + SQL + + expect(format_sql(Project.where(account_id: 1).eager_load(:categories).to_sql)) + .to eq(format_sql(expected_sql.strip)) project = Project.where(account_id: 1).eager_load(:categories).first expect(project.categories).to include(category1) - end end - it "test raw SQL joins" do + it 'test raw SQL joins' do account1 = Account.create! name: 'Account 1' category1 = Category.create! name: 'Category 1' MultiTenant.with(account1) do - expected_sql = if uses_prepared_statements? && (ActiveRecord::VERSION::MAJOR == 5 || (ActiveRecord::VERSION::MAJOR == 6 && ActiveRecord::VERSION::MINOR >= 1)) - <<-sql - SELECT "tasks".* FROM "tasks" INNER JOIN "projects" ON "projects"."id" = "tasks"."project_id" AND "projects"."account_id" = 1 LEFT JOIN project_categories pc ON project.category_id = pc.id WHERE "tasks"."account_id" = 1 - sql - else - <<-sql - SELECT "tasks".* FROM "tasks" INNER JOIN "projects" ON "projects"."account_id" = 1 AND "projects"."id" = "tasks"."project_id" LEFT JOIN project_categories pc ON project.category_id = pc.id WHERE "tasks"."account_id" = 1 - sql - end + option1 = <<-SQL.strip + SELECT "tasks".* FROM "tasks" + INNER JOIN "projects" ON "projects"."id" = "tasks"."project_id" AND "projects"."account_id" = 1 + LEFT JOIN project_categories pc ON project.category_id = pc.id#{' '} + WHERE "tasks"."account_id" = 1 + SQL + option2 = <<-SQL.strip + SELECT "tasks".* FROM "tasks" + INNER JOIN "projects" ON "projects"."account_id" = 1#{' '} + AND "projects"."id" = "tasks"."project_id" + LEFT JOIN project_categories pc ON project.category_id = pc.id#{' '} + WHERE "tasks"."account_id" = 1 + SQL project1 = Project.create! name: 'Project 1' - projectcategory = ProjectCategory.create! name: 'project cat 1', project: project1, category: category1 + ProjectCategory.create! name: 'project cat 1', project: project1, category: category1 project1.tasks.create! name: 'baz' - expect(Task.joins(:project).joins('LEFT JOIN project_categories pc ON project.category_id = pc.id').to_sql).to eq(expected_sql.strip) + expect( + format_sql( + Task.joins(:project).joins('LEFT JOIN project_categories pc ON project.category_id = pc.id').to_sql + ) + ).to eq(format_sql(option1)).or(eq(format_sql(option2))) end MultiTenant.without do - expected_sql = <<-sql - SELECT "tasks".* FROM "tasks" INNER JOIN "projects" ON "projects"."id" = "tasks"."project_id" AND "projects"."account_id" = "tasks"."account_id" LEFT JOIN project_categories pc ON project.category_id = pc.id WHERE "tasks"."account_id" = 1 - sql - - expect(Task.where(account_id: 1).joins(:project).joins('LEFT JOIN project_categories pc ON project.category_id = pc.id').to_sql).to eq(expected_sql.strip) - + expected_sql = <<-SQL.strip + SELECT "tasks".* FROM "tasks" + INNER JOIN "projects" ON "projects"."id" = "tasks"."project_id" + AND "tasks"."account_id" = "projects"."account_id" + LEFT JOIN project_categories pc ON project.category_id = pc.id + WHERE "tasks"."account_id" = 1 + SQL + + expect(format_sql(Task.where(account_id: 1).joins(:project) + .joins('LEFT JOIN project_categories pc ON project.category_id = pc.id') + .to_sql)).to eq(format_sql(expected_sql.strip)) end - end - it "only applies clauses when a tenant is set" do + it 'only applies clauses when a tenant is set' do account = Account.create! name: 'Account 1' project = Project.create! name: 'Project 1', account: account project2 = Project.create! name: 'Project 2', account: Account.create!(name: 'Account2') MultiTenant.with(account) do - expected_sql = if uses_prepared_statements? && ActiveRecord::VERSION::MAJOR > 5 - <<-sql.strip - SELECT "projects".* FROM "projects" WHERE "projects"."account_id" = #{account.id} AND "projects"."id" = $1 LIMIT $2 - sql - else - <<-sql.strip - SELECT "projects".* FROM "projects" WHERE "projects"."account_id" = #{account.id} AND "projects"."id" = $1 LIMIT $2 - sql - end - - expect(Project).to receive(:find_by_sql).with(expected_sql, any_args).and_call_original + option1 = <<-SQL.strip + SELECT "projects".* FROM "projects"#{' '} + WHERE "projects"."account_id" = #{account.id} AND "projects"."id" = $1 LIMIT $2 + SQL + option2 = <<-SQL.strip + SELECT "projects".* FROM "projects"#{' '} + WHERE "projects"."id" = $1 AND "projects"."account_id" = #{account.id} LIMIT $2 + SQL + option3 = <<-SQL.strip + SELECT "projects".* FROM "projects" + WHERE "projects"."id" = $1 + AND "projects"."account_id" = #{account.id} LIMIT $2 + SQL + + # Couldn't make the following line pass for some reason, so came up with an uglier alternative + # expect(Project).to receive(:find_by_sql).with(eq(option1). + # or(eq(option2)).or(eq(option3)), any_args).and_call_original + expect(Project).to receive(:find_by_sql).and_wrap_original do |m, *args| + expect(format_sql(args[0])).to(eq(format_sql(option1)) + .or(eq(format_sql(option2))).or(eq(format_sql(option3)))) + m.call(args[0], args[1], preparable: args[2][:preparable]) + end expect(Project.find(project.id)).to eq(project) end MultiTenant.without do - expected_sql = if uses_prepared_statements? && ActiveRecord::VERSION::MAJOR > 5 - <<-sql.strip - SELECT "projects".* FROM "projects" WHERE "projects"."id" = $1 LIMIT $2 - sql - else - <<-sql.strip - SELECT "projects".* FROM "projects" WHERE "projects"."id" = $1 LIMIT $2 - sql - end - - expect(Project).to receive(:find_by_sql).with(expected_sql, any_args).and_call_original + option1 = <<-SQL.strip + SELECT "projects".* FROM "projects"#{' '} + WHERE "projects"."id" = $1 LIMIT $2 + SQL + option2 = <<-SQL.strip + SELECT "projects".* FROM "projects"#{' '} + WHERE "projects"."id" = $1 LIMIT $2 + SQL + + # Couldn't make the following line pass for some reason, so came up with an uglier alternative + # expect(Project).to receive(:find_by_sql).with(eq(option1).or(eq(option2)), any_args).and_call_original + expect(Project).to receive(:find_by_sql).and_wrap_original do |m, *args| + expect(format_sql(args[0])).to(eq(format_sql(option1)).or(eq(format_sql(option2)))) + m.call(args[0], args[1], preparable: args[2][:preparable]) + end expect(Project.find(project2.id)).to eq(project2) end end - describe 'with unsaved association' do before do @account = Account.create!(name: 'reflection tenant') @@ -529,13 +741,13 @@ end end - it "test value of RETURNING insert in table with no pkey" do + it 'test value of RETURNING insert in table with no pkey' do account1 = Account.create(name: 'test1') MultiTenant.with(account1) do - allowed_place = AllowedPlace.create! name: 'something1' + AllowedPlace.create! name: 'something1' - project = Project.create! name: 'Project 1' + Project.create! name: 'Project 1' end end end diff --git a/spec/activerecord-multi-tenant/multi_tenant_spec.rb b/spec/activerecord-multi-tenant/multi_tenant_spec.rb index 89d91e69..f2f9fca9 100644 --- a/spec/activerecord-multi-tenant/multi_tenant_spec.rb +++ b/spec/activerecord-multi-tenant/multi_tenant_spec.rb @@ -1,8 +1,10 @@ +# frozen_string_literal: true + require 'spec_helper' RSpec.describe MultiTenant do - describe ".load_current_tenant!" do - let(:fake_tenant) { OpenStruct.new(id: 1) } + describe '.load_current_tenant!' do + let(:fake_tenant) { double(id: 1) } let(:mock_klass) { double(find: fake_tenant) } before do @@ -14,14 +16,14 @@ MultiTenant.default_tenant_class = @original_default_class end - it "sets and returns the loaded current_tenant" do + it 'sets and returns the loaded current_tenant' do expect(mock_klass).to receive(:find).once.with(1) MultiTenant.current_tenant = 1 expect(MultiTenant.load_current_tenant!).to eq(fake_tenant) expect(MultiTenant.current_tenant).to eq(fake_tenant) end - it "respects `.with` lifecycle" do + it 'respects `.with` lifecycle' do expect(mock_klass).to receive(:find).once.with(2) expect(MultiTenant.current_tenant).to eq(nil) MultiTenant.with(2) do @@ -31,34 +33,116 @@ expect(MultiTenant.current_tenant).to eq(nil) end - context "with a loaded current_tenant" do - it "returns the tenant without fetching it" do + context 'with a loaded current_tenant' do + it 'returns the tenant without fetching it' do expect(mock_klass).not_to receive(:find) MultiTenant.current_tenant = fake_tenant expect(MultiTenant.load_current_tenant!).to eq(fake_tenant) end end - context "with a nil current_tenant" do - it "raises an error, as there is not enough information to load the tenant" do + context 'with a nil current_tenant' do + it 'raises an error, as there is not enough information to load the tenant' do expect(mock_klass).not_to receive(:find) - expect { + expect do MultiTenant.load_current_tenant! - }.to raise_error(RuntimeError, 'MultiTenant.current_tenant must be set to load') + end.to raise_error(RuntimeError, 'MultiTenant.current_tenant must be set to load') end end - context "without a default class set" do + context 'without a default class set' do before do MultiTenant.default_tenant_class = nil end - it "raises an error, as there is not enough information to load the tenant" do + it 'raises an error, as there is not enough information to load the tenant' do expect(mock_klass).not_to receive(:find) MultiTenant.current_tenant = 1 - expect { + expect do MultiTenant.load_current_tenant! - }.to raise_error(RuntimeError, 'Only have tenant id, and no default tenant class set') + end.to raise_error(RuntimeError, 'Only have tenant id, and no default tenant class set') + end + end + end + + describe '.tenant_klass_defined?' do + context 'without options' do + before(:all) do + class SampleTenant < ActiveRecord::Base + multi_tenant :sample_tenant + end + end + + it 'return true with valid tenant_name' do + expect(MultiTenant.tenant_klass_defined?(:sample_tenant)).to eq(true) + end + + it 'return false with invalid_tenant_name' do + invalid_tenant_name = :tenant + expect(MultiTenant.tenant_klass_defined?(invalid_tenant_name)).to eq(false) + end + end + + context 'with options' do + context 'and valid class_name' do + it 'return true' do + class SampleTenant < ActiveRecord::Base + multi_tenant :tenant + end + + tenant_name = :tenant + options = { + class_name: 'SampleTenant' + } + expect(MultiTenant.tenant_klass_defined?(tenant_name, options)).to eq(true) + end + + it 'return true when tenant class is nested' do + module SampleModule + class SampleNestedTenant < ActiveRecord::Base + multi_tenant :tenant + end + # rubocop:disable Layout/TrailingWhitespace + # Trailing whitespace is intentionally left here + + class AnotherTenant < ActiveRecord::Base + end + # rubocop:enable Layout/TrailingWhitespace + end + tenant_name = :tenant + options = { + class_name: 'SampleModule::SampleNestedTenant' + } + expect(MultiTenant.tenant_klass_defined?(tenant_name, options)).to eq(true) + end + end + end + end + + describe '.wrap_methods' do + context 'when method is already prepended' do + it 'is not an stack error' do + klass = Class.new do + def hello + 'hello' + end + end + + klass.prepend(Module.new do + def hello + "#{super} world" + end + + def owner + Class.new(ActiveRecord::Base) do + self.table_name = 'accounts' + end.new + end + end) + + MultiTenant.wrap_methods(klass, :owner, :hello) + + expect(klass.new.hello).to eq('hello world') end end end diff --git a/spec/activerecord-multi-tenant/query_rewriter_spec.rb b/spec/activerecord-multi-tenant/query_rewriter_spec.rb index 19984770..db0edda8 100644 --- a/spec/activerecord-multi-tenant/query_rewriter_spec.rb +++ b/spec/activerecord-multi-tenant/query_rewriter_spec.rb @@ -1,101 +1,248 @@ +# frozen_string_literal: true + require 'spec_helper' -describe "Query Rewriter" do +describe 'Query Rewriter' do + before(:each) do + @queries = [] + ActiveSupport::Notifications.subscribe('sql.active_record') do |_name, _started, _finished, _unique_id, payload| + @queries << payload[:sql] + end + end - context "when bulk updating" do - let!(:account) { Account.create!(name: "Test Account") } - let!(:project) { Project.create(name: "Project 1", account: account) } - let!(:manager) { Manager.create(name: "Manager", project: project, account: account) } + context 'when bulk updating' do + let!(:account) { Account.create!(name: 'Test Account') } + let!(:project) { Project.create(name: 'Project 1', account: account) } + let!(:manager) { Manager.create(name: 'Manager', project: project, account: account) } - it "updates the records" do - expect { + it 'updates the records' do + expect do MultiTenant.with(account) do - Project.joins(:manager).update_all(name: "New Name") + Project.joins(:manager).update_all(name: 'New Name') end - }.to change { project.reload.name }.from("Project 1").to("New Name") + end.to change { project.reload.name }.from('Project 1').to('New Name') end - it "updates the records without a current tenant" do - expect { - Project.joins(:manager).update_all(name: "New Name") - }.to change { project.reload.name }.from("Project 1").to("New Name") + it 'updates the records without a current tenant' do + expect do + Project.joins(:manager).update_all(name: 'New Name') + end.to change { project.reload.name }.from('Project 1').to('New Name') end - it "update the record" do - expect { + it 'update the record' do + expect do MultiTenant.with(account) do - project.update(name: "New Name") + project.update(name: 'New Name') end - }.to change { project.reload.name }.from("Project 1").to("New Name") + end.to change { project.reload.name }.from('Project 1').to('New Name') + end + + it 'update the record without a current tenant' do + expect do + project.update(name: 'New Name') + end.to change { project.reload.name }.from('Project 1').to('New Name') end - it "update the record without a current tenant" do - expect { - project.update(name: "New Name") - }.to change { project.reload.name }.from("Project 1").to("New Name") + it 'update_all the records with expected query' do + expected_query = <<-SQL.strip + UPDATE "projects" SET "name" = 'New Name' WHERE "projects"."id" IN + (SELECT "projects"."id" FROM "projects" + INNER JOIN "managers" ON "managers"."project_id" = "projects"."id" + and "managers"."account_id" = :account_id + WHERE "projects"."account_id" = :account_id + ) + AND "projects"."account_id" = :account_id + SQL + + expect do + MultiTenant.with(account) do + Project.joins(:manager).update_all(name: 'New Name') + end + end.to change { project.reload.name }.from('Project 1').to('New Name') + + @queries.each do |actual_query| + next unless actual_query.include?('UPDATE "projects" SET "name"') + + expect(format_sql(actual_query)).to eq(format_sql(expected_query.gsub(':account_id', account.id.to_s))) + end + end + + it 'updates a limited number of records with expected query' do + # create 2 more projects + Project.create(name: 'project2', account: account) + Project.create(name: 'project3', account: account) + new_name = 'New Name' + limit = 2 + expected_query = <<-SQL + UPDATE + "projects" + SET + "name" = 'New Name' + WHERE + "projects"."id" IN ( + SELECT + "projects"."id" + FROM + "projects" + WHERE + "projects"."account_id" = #{account.id} LIMIT #{limit} + ) + AND "projects"."account_id" = #{account.id} + SQL + + expect do + MultiTenant.with(account) do + Project.limit(limit).update_all(name: new_name) + end + end.to change { Project.where(name: new_name).count }.from(0).to(limit) + + @queries.each do |actual_query| + next unless actual_query.include?('UPDATE "projects" SET "name"') + + expect(format_sql(actual_query.gsub('$1', + limit.to_s)).strip).to eq(format_sql(expected_query).strip) + end end end - context "when bulk deleting" do - let!(:account) { Account.create!(name: "Test Account") } - let!(:project1) { Project.create(name: "Project 1", account: account) } - let!(:project2) { Project.create(name: "Project 2", account: account) } - let!(:project3) { Project.create(name: "Project 3", account: account) } - let!(:manager1) { Manager.create(name: "Manager 1", project: project1, account: account) } - let!(:manager2) { Manager.create(name: "Manager 2", project: project2, account: account) } + context 'when bulk deleting' do + let!(:account) { Account.create!(name: 'Test Account') } + let!(:project1) { Project.create(name: 'Project 1', account: account) } + let!(:project2) { Project.create(name: 'Project 2', account: account) } + let!(:project3) { Project.create(name: 'Project 3', account: account) } + let!(:manager1) { Manager.create(name: 'Manager 1', project: project1, account: account) } + let!(:manager2) { Manager.create(name: 'Manager 2', project: project2, account: account) } + + before(:each) do + @queries = [] + ActiveSupport::Notifications.subscribe('sql.active_record') do |_name, _started, _finished, _unique_id, payload| + @queries << payload[:sql] + end + end + + after(:each) do + ActiveSupport::Notifications.unsubscribe('sql.active_record') + end + + it 'delete_all the records' do + expected_query = <<-SQL.strip + DELETE FROM "projects" WHERE "projects"."id" IN + (SELECT "projects"."id" FROM "projects" + INNER JOIN "managers" ON "managers"."project_id" = "projects"."id" + and "managers"."account_id" = :account_id + WHERE "projects"."account_id" = :account_id + ) + AND "projects"."account_id" = :account_id + SQL - it "delete_all the records" do - expect { + expect do MultiTenant.with(account) do Project.joins(:manager).delete_all end - }.to change { Project.count }.from(3).to(1) + end.to change { Project.count }.from(3).to(1) + + @queries.each do |actual_query| + next unless actual_query.include?('DELETE FROM ') + + expect(format_sql(actual_query)).to eq(format_sql(expected_query.gsub(':account_id', account.id.to_s))) + end end - it "delete_all the records without a current tenant" do - expect { + it 'delete_all the records without a current tenant' do + expect do Project.joins(:manager).delete_all - }.to change { Project.count }.from(3).to(1) + end.to change { Project.count }.from(3).to(1) end - it "delete the record" do - expect { + it 'delete the record' do + expect do MultiTenant.with(account) do project1.delete Project.delete(project2.id) end - }.to change { Project.count }.from(3).to(1) + end.to change { Project.count }.from(3).to(1) end - it "delete the record without a current tenant" do - expect { + it 'delete the record without a current tenant' do + expect do project1.delete Project.delete(project2.id) - }.to change { Project.count }.from(3).to(1) + end.to change { Project.count }.from(3).to(1) end - it "destroy the record" do - expect { + it 'deletes a limited number of records with expected query' do + # create 2 more projects + Project.create(name: 'project2', account: account) + Project.create(name: 'project3', account: account) + limit = 2 + expected_query = <<-SQL + DELETE FROM + "projects" + WHERE + "projects"."id" IN ( + SELECT + "projects"."id" + FROM + "projects" + WHERE + "projects"."account_id" = #{account.id} LIMIT #{limit} + ) + AND "projects"."account_id" = #{account.id} + SQL + + expect do + MultiTenant.with(account) do + Project.limit(limit).delete_all + end + end.to change { Project.count }.by(-limit) + + @queries.each do |actual_query| + next unless actual_query.include?('DELETE FROM "projects"') + + expect(format_sql(actual_query.gsub('$1', + limit.to_s)).strip).to eq(format_sql(expected_query).strip) + end + end + + it 'destroy the record' do + expect do MultiTenant.with(account) do project1.destroy Project.destroy(project2.id) end - }.to change { Project.count }.from(3).to(1) + end.to change { Project.count }.from(3).to(1) end - it "destroy the record without a current tenant" do - expect { + it 'destroy the record without a current tenant' do + expect do project1.destroy Project.destroy(project2.id) - }.to change { Project.count }.from(3).to(1) + end.to change { Project.count }.from(3).to(1) + end + end + + context 'when update without arel' do + it 'can call method' do + expect do + ActiveRecord::Base.connection.update('SELECT 1') + end.not_to raise_error end end - context "when update without arel" do - it "can call method" do - expect { - ActiveRecord::Base.connection.update("SELECT 1") - }.not_to raise_error + context 'when joining with a model with a default scope' do + let!(:account) { Account.create!(name: 'Test Account') } + + it 'fetches only records within the default scope' do + alive = Domain.create(name: 'alive', account: account) + deleted = Domain.create(name: 'deleted', deleted: true, account: account) + page_in_alive_domain = Page.create(name: 'alive', account: account, domain: alive) + Page.create(name: 'deleted', account: account, domain: deleted) + + expect( + MultiTenant.with(account) do + Page.joins(:domain).pluck(:id) + end + ).to eq([page_in_alive_domain.id]) end end end diff --git a/spec/activerecord-multi-tenant/record_callback_spec.rb b/spec/activerecord-multi-tenant/record_callback_spec.rb index 4a7e64f0..816fb678 100644 --- a/spec/activerecord-multi-tenant/record_callback_spec.rb +++ b/spec/activerecord-multi-tenant/record_callback_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' class ProjectWithCallbacks < ActiveRecord::Base diff --git a/spec/activerecord-multi-tenant/record_finding_spec.rb b/spec/activerecord-multi-tenant/record_finding_spec.rb index 6bda9cd5..d5b46074 100644 --- a/spec/activerecord-multi-tenant/record_finding_spec.rb +++ b/spec/activerecord-multi-tenant/record_finding_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe MultiTenant, 'Record finding' do @@ -61,36 +63,36 @@ end context 'model with has_many relation through multi-tenant model' do - let(:tenant_1) { Account.create! name: 'Tenant 1' } - let(:project_1) { tenant_1.projects.create! } + let(:tenant1) { Account.create! name: 'Tenant 1' } + let(:project1) { tenant1.projects.create! } - let(:tenant_2) { Account.create! name: 'Tenant 2' } - let(:project_2) { tenant_2.projects.create! } + let(:tenant2) { Account.create! name: 'Tenant 2' } + let(:project2) { tenant2.projects.create! } let(:category) { Category.create! name: 'Category' } before do - ProjectCategory.create! account: tenant_1, name: '1', project: project_1, category: category - ProjectCategory.create! account: tenant_2, name: '2', project: project_2, category: category + ProjectCategory.create! account: tenant1, name: '1', project: project1, category: category + ProjectCategory.create! account: tenant2, name: '2', project: project2, category: category end it 'can get model without creating query cache' do - MultiTenant.with(tenant_1) do - found_category = Project.find(project_1.id).categories.to_a.first + MultiTenant.with(tenant1) do + found_category = Project.find(project1.id).categories.to_a.first expect(found_category).to eq(category) end end it 'can get model for other tenant' do - MultiTenant.with(tenant_2) do - found_category = Project.find(project_2.id).categories.to_a.first + MultiTenant.with(tenant2) do + found_category = Project.find(project2.id).categories.to_a.first expect(found_category).to eq(category) end end it 'can get model without current_tenant' do MultiTenant.without do - found_category = Project.find(project_2.id).categories.to_a.first + found_category = Project.find(project2.id).categories.to_a.first expect(found_category).to eq(category) end end diff --git a/spec/activerecord-multi-tenant/record_modifications_spec.rb b/spec/activerecord-multi-tenant/record_modifications_spec.rb index 76376177..01423f82 100644 --- a/spec/activerecord-multi-tenant/record_modifications_spec.rb +++ b/spec/activerecord-multi-tenant/record_modifications_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe MultiTenant, 'Record modifications' do @@ -6,7 +8,6 @@ let(:project) { Project.create! name: 'something', account: account } let(:project2) { Project.create! name: 'something2', account: account2, id: project.id } - it 'includes the tenant_id in DELETEs when using object.destroy' do # two records with same id but different account_id # when doing project.destroy it should delete only the current one @@ -16,7 +17,7 @@ expect(project2.account).to eq(account2) expect(project.id).to eq(project2.id) - MultiTenant.without() do + MultiTenant.without do expect(Project.count).to eq(2) project.destroy expect(Project.count).to eq(1) @@ -28,7 +29,6 @@ MultiTenant.with(account2) do expect(Project.where(id: project2.id).first).to be_present end - end it 'includes the tenant_id in DELETEs when using object.delete' do @@ -40,7 +40,7 @@ expect(project2.account).to eq(account2) expect(project.id).to eq(project2.id) - MultiTenant.without() do + MultiTenant.without do expect(Project.count).to eq(2) project.delete expect(Project.count).to eq(1) @@ -54,6 +54,25 @@ end end + it 'should not update other objects with same id when calling object.update_columns' do + # When two records with same id but different account_id are updated, it should only update the current one + expect(project.account).to eq(account) + expect(project2.account).to eq(account2) + expect(project.id).to eq(project2.id) + + MultiTenant.without do + project2.update_columns(name: 'newthing2') + expect(project.reload.name).to eq('something') + expect(project2.reload.name).to eq('newthing2') + end + end + + it 'should return the same object when calling object.reload' do + # When two records with same id but different account_id are updated, it should not return the other object + expect(project.reload.account_id).to eq(account.id) + expect(project2.reload.account_id).to eq(account2.id) + end + it 'test delete for reference tables' do category1 = Category.create! name: 'Category 1' expect(Category.count).to eq(1) diff --git a/spec/activerecord-multi-tenant/schema_dumper_tester.rb b/spec/activerecord-multi-tenant/schema_dumper_tester.rb deleted file mode 100644 index e69de29b..00000000 diff --git a/spec/activerecord-multi-tenant/sidekiq_spec.rb b/spec/activerecord-multi-tenant/sidekiq_spec.rb index 7307e8a7..30b9c792 100644 --- a/spec/activerecord-multi-tenant/sidekiq_spec.rb +++ b/spec/activerecord-multi-tenant/sidekiq_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' require 'sidekiq/client' require 'activerecord-multi-tenant/sidekiq' @@ -5,18 +7,29 @@ describe MultiTenant, 'Sidekiq' do let(:server) { Sidekiq::Middleware::MultiTenant::Server.new } let(:account) { Account.create(name: 'test') } + let(:deleted_account) { Account.create(name: 'deleted') } + + before { deleted_account.destroy! } describe 'server middleware' do it 'sets the multitenant context when provided in message' do - server.call(double,{'bogus' => 'message', - 'multi_tenant' => { 'class' => account.class.name, 'id' => account.id}}, - 'bogus_queue') do + server.call(double, { 'bogus' => 'message', + 'multi_tenant' => { 'class' => account.class.name, 'id' => account.id } }, + 'bogus_queue') do expect(MultiTenant.current_tenant).to eq(account) end end + it 'sets the multitenant context (id) even if tenant not found' do + server.call(double, { 'bogus' => 'message', + 'multi_tenant' => { 'class' => deleted_account.class.name, 'id' => deleted_account.id } }, + 'bogus_queue') do + expect(MultiTenant.current_tenant).to eq(deleted_account.id) + end + end + it 'does not set the multitenant context when no tenant provided' do - server.call(double, {'bogus' => 'message'}, 'bogus_queue') do + server.call(double, { 'bogus' => 'message' }, 'bogus_queue') do expect(MultiTenant.current_tenant).to be_nil end end diff --git a/spec/schema.rb b/spec/schema.rb index 86340503..517cf965 100644 --- a/spec/schema.rb +++ b/spec/schema.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Resets the database, except when we are only running a specific spec ARGV.grep(/\w+_spec\.rb/).empty? && ActiveRecord::Schema.define(version: 1) do enable_extension_on_all_nodes 'uuid-ossp' @@ -7,6 +9,7 @@ t.column :name, :string t.column :subdomain, :string t.column :domain, :string + t.column :password, :string end create_table :projects, force: true, partition_key: :account_id do |t| @@ -27,6 +30,17 @@ t.column :completed, :boolean end + create_table :managers_tasks, force: true, partition_key: :account_id do |t| + t.column :account_id, :integer + t.column :manager_id, :integer + t.column :task_id, :integer + end + + create_table :managers_projects, force: true do |t| + t.column :project_id, :integer + t.column :manager_id, :integer + end + create_table :sub_tasks, force: true, partition_key: :account_id do |t| t.column :account_id, :integer t.column :name, :string @@ -34,6 +48,13 @@ t.column :type, :string end + create_table :optional_sub_tasks, force: true do |t| + t.references :account, :integer + t.column :sub_task_id, :integer + t.column :name, :string + t.column :type, :string + end + create_table :countries, force: true do |t| t.column :name, :string end @@ -89,10 +110,26 @@ t.column :category_id, :integer end - create_table :allowed_places, force: true, id: false do |t| - t.string :account_id, :integer - t.string :name, :string + t.string :account_id, :integer + t.string :name, :string + end + + create_table :domains, force: true, partition_key: :account_id do |t| + t.column :account_id, :integer + t.column :name, :string + t.column :deleted, :boolean, default: false + end + + create_table :pages, force: true, partition_key: :account_id do |t| + t.column :account_id, :integer + t.column :name, :string + t.column :domain_id, :integer + end + + create_table :posts, force: true, partition_key: :account_id do |t| + t.column :account_id, :integer + t.column :name, :string end create_distributed_table :accounts, :id @@ -108,6 +145,9 @@ create_distributed_table :uuid_records, :organization_id create_distributed_table :project_categories, :account_id create_distributed_table :allowed_places, :account_id + create_distributed_table :domains, :account_id + create_distributed_table :pages, :account_id + create_distributed_table :posts, :account_id create_reference_table :categories end @@ -115,6 +155,7 @@ class Account < ActiveRecord::Base multi_tenant :account has_many :projects has_one :manager, inverse_of: :account + has_many :optional_sub_tasks end class Project < ActiveRecord::Base @@ -125,6 +166,7 @@ class Project < ActiveRecord::Base has_many :project_categories has_many :categories, through: :project_categories + has_and_belongs_to_many :managers validates_uniqueness_of :name, scope: [:account] end @@ -132,12 +174,15 @@ class Project < ActiveRecord::Base class Manager < ActiveRecord::Base multi_tenant :account belongs_to :project + has_and_belongs_to_many :tasks, tenant_column: :account_id, tenant_enabled: true, + tenant_class_name: 'Account' end class Task < ActiveRecord::Base multi_tenant :account belongs_to :project has_many :sub_tasks + has_and_belongs_to_many :managers, tenant_column: :account_id, tenant_enabled: true validates_uniqueness_of :name end @@ -146,6 +191,14 @@ class SubTask < ActiveRecord::Base multi_tenant :account belongs_to :task has_one :project, through: :task + has_many :optional_sub_tasks +end + +with_belongs_to_required_by_default do + class OptionalSubTask < ActiveRecord::Base + multi_tenant :account, optional: true + belongs_to :sub_task + end end class StiSubTask < SubTask @@ -181,10 +234,11 @@ class SubclassTask < AbstractTask class Comment < ActiveRecord::Base multi_tenant :account belongs_to :commentable, polymorphic: true - belongs_to :task, -> { where(comments: { commentable_type: 'Task' }) }, foreign_key: 'commentable_id' + belongs_to :task, -> { where(comments: { commentable_type: 'Task' }) }, foreign_key: 'commentable_id' end class Organization < ActiveRecord::Base + multi_tenant :organization has_many :uuid_records end @@ -193,7 +247,7 @@ class UuidRecord < ActiveRecord::Base end class Category < ActiveRecord::Base - has_many :project_categories + has_many :project_categories has_many :projects, through: :project_categories end @@ -204,7 +258,17 @@ class ProjectCategory < ActiveRecord::Base belongs_to :account end - class AllowedPlace < ActiveRecord::Base multi_tenant :account end + +class Domain < ActiveRecord::Base + multi_tenant :account + has_many :pages + default_scope { where(deleted: false) } +end + +class Page < ActiveRecord::Base + multi_tenant :account + belongs_to :domain +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 04c43c4e..28628ff3 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,17 +1,57 @@ +# frozen_string_literal: true + $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) $LOAD_PATH.unshift(File.dirname(__FILE__)) +# Codecov is enabled when CI is set to true +if ENV['CI'] == 'true' + puts 'Enabling Simplecov to upload code coverage results to codecov.io' + require 'simplecov' + SimpleCov.start 'rails' do + add_filter '/test/' # Exclude test directory from coverage + add_filter '/spec/' # Exclude spec directory from coverage + add_filter '/config/' # Exclude config directory from coverage + + # Add any additional filters or exclusions if needed + # add_filter '/other_directory/' + + add_group 'Lib', '/lib' # Include the lib directory for coverage + puts "Tracked files: #{SimpleCov.tracked_files}" + end + SimpleCov.minimum_coverage 80 + + require 'simplecov-cobertura' + SimpleCov.formatter = SimpleCov::Formatter::CoberturaFormatter +end + require 'active_record/railtie' require 'action_controller/railtie' require 'rspec/rails' -require 'activerecord-multi-tenant' +module MultiTenantTest + class Application < Rails::Application; end +end + +# Specifies columns which shouldn't be exposed while calling #inspect. +ActiveSupport.on_load(:active_record) do + self.filter_attributes += MultiTenantTest::Application.config.filter_parameters +end + +require 'activerecord_multi_tenant' + +# It's necessary for testing the filtering of senstive column values in ActiveRecord. +# Refer to "describe 'inspect method filters senstive column values'" +# +# To verify that ActiveSupport.on_load(:active_record) is not being unnecessarily invoked, +# this line should be placed after "require 'activerecord_multi_tenant'" and before ActiveRecord::Base is called. +MultiTenantTest::Application.config.filter_parameters = [:password] require 'bundler' Bundler.require(:default, :development) +require_relative 'support/format_sql' -dbconfig = YAML::load(IO.read(File.join(File.dirname(__FILE__), 'database.yml'))) -ActiveRecord::Base.logger = Logger.new(File.join(File.dirname(__FILE__), "debug.log")) +dbconfig = YAML.safe_load_file(File.join(File.dirname(__FILE__), 'database.yml')) +ActiveRecord::Base.logger = Logger.new(File.join(File.dirname(__FILE__), 'debug.log')) ActiveRecord::Base.establish_connection(dbconfig['test']) RSpec.configure do |config| @@ -25,9 +65,6 @@ config.before(:suite) do MultiTenant::FastTruncate.run - - # Keep this here until https://github.com/citusdata/citus/issues/1236 is fixed - MultiTenant.enable_with_lock_workaround end config.after(:each) do @@ -35,15 +72,14 @@ end end -module MultiTenantTest - class Application < Rails::Application; end -end - -MultiTenantTest::Application.config.secret_token = 'x' * 40 -MultiTenantTest::Application.config.secret_key_base = 'y' * 40 - -def uses_prepared_statements? - ActiveRecord::Base.connection.prepared_statements +# rubocop:disable Lint/UnusedMethodArgument +# changing the name of the parameter breaks tests +def with_belongs_to_required_by_default(&block) + default_value = ActiveRecord::Base.belongs_to_required_by_default + ActiveRecord::Base.belongs_to_required_by_default = true + yield +ensure + ActiveRecord::Base.belongs_to_required_by_default = default_value end - +# rubocop:enable Lint/UnusedMethodArgument require 'schema' diff --git a/spec/support/format_sql.rb b/spec/support/format_sql.rb new file mode 100644 index 00000000..e9c0e220 --- /dev/null +++ b/spec/support/format_sql.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +require 'anbt-sql-formatter/formatter' + +module SQLFormatter + def format_sql(sql) + rule = AnbtSql::Rule.new + rule.keyword = AnbtSql::Rule::KEYWORD_UPPER_CASE + %w[count sum substr date].each do |func_name| + rule.function_names << func_name.upcase + end + rule.indent_string = ' ' + formatter = AnbtSql::Formatter.new(rule) + formatter.format(sql.dup) + end +end + +RSpec.configure do |config| + config.include SQLFormatter +end