Class: ActiveStash::IndexDSL

Inherits:
Object
  • Object
show all
Defined in:
lib/active_stash/index_dsl.rb

Overview

Implements the DSL for adding searchable encrypted indexes to ActiveRecord models that include ActiveStash::Search.

stash_index do
  auto :email, :first_name, :last_name
  unique :email
  range :created_at, :updated_at
  exact :gender

  match :description, filter_term_bits: 512

  match_all :first_name, :last_name, :email

  index_assoc :patient do
    auto :email, :first_name, :last_name
  end
end

Instance Method Summary collapse

Constructor Details

#initialize(model_class, path = [], reflector = nil) ⇒ IndexDSL

Returns a new instance of IndexDSL.



21
22
23
24
25
26
27
28
29
30
# File 'lib/active_stash/index_dsl.rb', line 21

def initialize(model_class, path = [], reflector = nil)
  @model_class = model_class
  @reflector = reflector || Reflector.new(@model_class)
  @path = path || []
  @is_in_association = path.size > 0
  @indexes = []
  @associations = []
  @unique_fields = []
  @callback_registration_handlers = []
end

Instance Method Details

#auto(*fields) ⇒ Object

Automatically defines all applicable index types on one or more fields.



60
61
62
63
64
65
66
67
68
69
70
71
# File 'lib/active_stash/index_dsl.rb', line 60

def auto(*fields)
  fields.each do |name|
    if @reflector.fields.include?(name.to_s)
      field_type = @reflector.fields[name.to_s]
      Index.applicable_index_types(field_type).each do |index_type|
        @indexes.push(Index.send(index_type, name.to_s))
      end
    else
      raise ConfigError, "Attempted to auto index an unknown attribute '#{name}' on model '#{@model_class}'"
    end
  end
end

#exact(*fields) ⇒ Object

Defines an exact index on one or more fields.



74
75
76
77
78
79
80
81
82
83
84
85
86
87
# File 'lib/active_stash/index_dsl.rb', line 74

def exact(*fields)
  fields.each do |name|
    if @reflector.fields.include?(name.to_s)
      field_type = @reflector.fields[name.to_s]
      if Index.valid_index_type_for_field_type?(:exact, field_type)
        @indexes.push(Index.exact(name.to_s))
      else
        raise ConfigError, "Attempted to create an exact index on type that does not support an exact index (attribute '#{name}' of model '#{@model_class})'"
      end
    else
      raise ConfigError, "Attempted to apply an exact index an unknown attribute '#{name}' on model '#{@model_class}'"
    end
  end
end

#finalize!Object

Performs the following checks and operations before returning the final indexes.

  1. Validates that there are not multiple index definitions of the same

type for the same field.

  1. Validates that there are not multiple associated indexes defined for

the same association.

  1. Processes `unique` indexes by either:

    • Updating existing `range` and `exact` indexes for a field to enforce

    uniqueness

    • Creating an `exact` or `range` index for the field if one does not

    already exist (`string` and `text` fields generate `exact` indexes, everything else generates a `range` index).



50
51
52
53
54
55
56
# File 'lib/active_stash/index_dsl.rb', line 50

def finalize!
  return @finalized_config if @finalized_config
  validate_no_fields_with_duplicate_index_definitions!
  validate_no_association_duplicates!
  process_unique_indexes!
  @finalized_config = ActiveStash::FinalizedIndexConfig.new(@indexes, @callback_registration_handlers)
end

#index_assoc(association, &block) ⇒ Object

Pulls fields from an association into the index on this model. This is a form of denormalisation.

This mechanism works whether the foreign model includes ActiveStash::Search or not.

Currently, it is only possible to index `has_one` or `belongs_to` associations.



168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
# File 'lib/active_stash/index_dsl.rb', line 168

def index_assoc(association, &block)
  unless ActiveStash::ModelReflection.association_names(@model_class).include?(association)
    raise ConfigError, "No such association '#{association}' on model '#{@model_class}'"
  end

  unless @path.size == 0
    raise ConfigError, "Nested association indexing is currently not supported"
  end

  associated_model = ActiveStash::ModelReflection.associated_model(@model_class, association)

  path = [*@path, association]
  dsl = IndexDSL.new(associated_model, @path, Reflector.new(associated_model))

  reflection = @model_class.reflect_on_association(association)

  unless [:has_one, :belongs_to].include?(reflection.macro)
    raise ConfigError, "Only 1-to-1 associations (belongs_to and has_one) are currently supported"
  end

  dsl.instance_eval(&block)
  association_indexes = dsl.finalize!.indexes
  @indexes.concat(association_indexes.indexes.map do |idx|
    idx.tap { |i| i.name = "#{path.join(".")}.#{i.name}" }
  end)


  inverse_name = reflection.inverse_of.name

  @callback_registration_handlers.push(-> {
    reflection.klass.after_save do |record|
      record.send(inverse_name).try(:cs_put)
    end

    reflection.klass.after_destroy do |record|
      # This will reindex every stash index associated with the model, not
      # only the index that needs updating.  We can get smarter about this
      # in the future.
      record.send(inverse_name).reload
      record.send(inverse_name).try(:cs_put)
    end
  })
end

#match(*fields, **opts) ⇒ Object



104
105
106
107
108
109
110
111
112
113
114
115
116
117
# File 'lib/active_stash/index_dsl.rb', line 104

def match(*fields, **opts)
  fields.each do |name|
    if @reflector.fields.include?(name.to_s)
      field_type = @reflector.fields[name.to_s]
      if Index.valid_index_type_for_field_type?(:match, field_type)
        @indexes.push(Index.match(name.to_s, **opts))
      else
        raise ConfigError, "Attempted to create a match index on type that does not support an range index (attribute '#{name}' of model '#{@model_class})'"
      end
    else
      raise ConfigError, "Attempted to apply a match index an unknown attribute '#{name}' on model '#{@model_class}'"
    end
  end
end

#match_all(*fields, **opts) ⇒ Object

Raises:



119
120
121
122
123
124
125
126
127
128
129
130
# File 'lib/active_stash/index_dsl.rb', line 119

def match_all(*fields, **opts)
  raise ConfigError, "No fields specified for match_all" if fields.size == 0

  fields.each do |f|
    field_type = @reflector.fields[f.to_s]
    unless Index.valid_index_type_for_field_type?(:match, field_type)
      raise ConfigError, "Only attributes of type string or text can be used in a match_all index (attribute #{f} is of type #{field_type}) in model '#{@model_class}'"
    end
  end

  @indexes.push(Index.match_multi(fields, **opts))
end

#range(*fields) ⇒ Object



89
90
91
92
93
94
95
96
97
98
99
100
101
102
# File 'lib/active_stash/index_dsl.rb', line 89

def range(*fields)
  fields.each do |name|
    if @reflector.fields.include?(name.to_s)
      field_type = @reflector.fields[name.to_s]
      if Index.valid_index_type_for_field_type?(:range, field_type)
        @indexes.push(Index.range(name))
      else
        raise ConfigError, "Attempted to create a range index on type that does not support an range index (attribute '#{name}' of model '#{@model_class})'"
      end
    else
      raise ConfigError, "Attempted to apply a range index an unknown attribute '#{name}' on model '#{@model_class}'"
    end
  end
end

#unique(field) ⇒ Object

Defines a unique index on a single field.

If no exact or range index already exists on the field, `unique` will create a new exact index with a unique constraint.

All existing exact and range indexes for the field will be modified to enforce a unique constraint, except if the field is a `string` or `text` type, in which case the unique constraint will only be applied to the `exact` index. This is because `range` indexes on strings are lossy and could cause false positive uniqueness checks.



143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
# File 'lib/active_stash/index_dsl.rb', line 143

def unique(field)
  if @is_in_association
    raise ConfigError, "Attempted to create a unique constraint on an associated model"
  end

  if @reflector.fields.include?(field.to_s)
    if @unique_fields.include?(field)
      raise ConfigError, "A unique constraint is already defined on '#{field}' on model '#{@model_class})"
    else
      @unique_fields.push(field)
    end
  else
    raise ConfigError, "Attempted to create a unique constraint on unknown attribute '#{field}' on model '#{@model_class})"
  end
end