Module: Sublet::Core

Defined in:
lib/sublet/core.rb

Class Method Summary collapse

Class Method Details

.build_nested_joins(joins_path) ⇒ Object

Build nested joins hash from an array of association names [:account, :company] => { account: :company } [:department] => :department



86
87
88
89
90
# File 'lib/sublet/core.rb', line 86

def build_nested_joins(joins_path)
  return nil if joins_path.nil? || joins_path.empty?

  joins_path.reverse.reduce(nil) { |acc, name| acc ? { name => acc } : name }
end

.extract_tenant_id(record) ⇒ Object

Extract tenant id from a record instance by walking to the last hop and checking a FK or assoc.



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
# File 'lib/sublet/core.rb', line 93

def extract_tenant_id(record)
  tenant = Sublet.current_tenant
  return nil unless tenant

  path = Sublet::PathResolver.resolve_path_to_tenant(record.class)
  return nil unless path

  obj = record
  path.each do |assoc|
    return nil unless obj.respond_to?(assoc)

    obj = obj.public_send(assoc)
    return nil if obj.nil?
  end

  if (bt = obj.class.reflect_on_all_associations(:belongs_to).find { |r| r.klass == tenant.class })
    return obj.public_send(bt.foreign_key)
  end

  assoc_name_guess = tenant.class.name.demodulize.underscore
  if obj.respond_to?(assoc_name_guess)
    t = obj.public_send(assoc_name_guess)
    return t&.id
  end

  nil
end

.scoped_relation_for(model, relation = model.all) ⇒ Object

Build a scoped relation that limits relation (for model) to the current tenant, if present.



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
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
# File 'lib/sublet/core.rb', line 8

def scoped_relation_for(model, relation = model.all)
  tenant = Sublet.current_tenant
  return relation unless tenant

  path = Sublet::PathResolver.resolve_path_to_tenant(model)
  return relation unless path && !path.empty? # fail-open if no path or tenant root not found

  # Build deterministic INNER JOIN SQL across the whole chain to avoid AR 8 alias tracker issues
  rel = relation
  current_klass = model
  current_table = current_klass.table_name
  join_sql_parts = []

  path.each do |assoc|
    reflection = current_klass.reflect_on_association(assoc)
    return relation unless reflection

    next_klass = reflection.klass
    next_table = next_klass.table_name

    case reflection.macro
    when :belongs_to
      # current_table.fk = next_table.pk
      fk = reflection.foreign_key
      pk = reflection.association_primary_key
      join_sql_parts << "INNER JOIN \"#{next_table}\" ON \"#{current_table}\".\"#{fk}\" = " \
                        "\"#{next_table}\".\"#{pk}\""
      current_klass = next_klass
      current_table = next_table
    when :has_one, :has_many
      # next_table.fk = current_table.pk
      fk = reflection.foreign_key
      pk = reflection.active_record_primary_key
      join_sql_parts << "INNER JOIN \"#{next_table}\" ON \"#{next_table}\".\"#{fk}\" = " \
                        "\"#{current_table}\".\"#{pk}\""
      current_klass = next_klass
      current_table = next_table
    else
      # unsupported macro; fail open
      return relation
    end
  end

  rel = rel.joins(join_sql_parts.join(' ')) if join_sql_parts.any?

  # Walk the path to discover the last klass we joined to
  last_klass = model
  path.each do |assoc|
    reflection = last_klass.reflect_on_association(assoc)
    return relation unless reflection

    last_klass = reflection.klass
  end

  tenant_table = tenant.class.table_name
  last_table   = last_klass.table_name

  # Case 1: the last hop IS the tenant class (e.g., ... -> Company)
  return rel.where(tenant_table => { id: tenant.id }).distinct if last_klass == tenant.class

  # Case 2: the last hop has a direct belongs_to to the tenant class
  if (bt = last_klass.reflect_on_all_associations(:belongs_to).find { |r| r.klass == tenant.class })
    return rel.where(last_table => { bt.foreign_key => tenant.id }).distinct
  end

  # Case 3: try a conventional association name matching the tenant class (e.g., :company)
  assoc_name_guess = tenant.class.name.demodulize.underscore.to_sym
  if last_klass.reflect_on_association(assoc_name_guess)
    return rel.joins(assoc_name_guess).where(tenant_table => { id: tenant.id }).distinct
  end

  # Could not determine how to constrain; fail open
  relation
end