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, 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 { |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, options)
        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