Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Issue/320 index name based current tenant #1

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ You can find the full reference on [Algolia's website](https://www.algolia.com/d
* [Auto-indexing & asynchronism](#auto-indexing--asynchronism)
* [Custom index name](#custom-index-name)
* [Per-environment indices](#per-environment-indices)
* [Per-tenant indices](#per-tenant-indices)
* [Custom attribute definition](#custom-attribute-definition)
* [Nested objects/relations](#nested-objectsrelations)
* [Custom <code>objectID</code>](#custom-codeobjectidcode)
Expand Down Expand Up @@ -609,6 +610,22 @@ class Contact < ActiveRecord::Base
end
```


## Per-tenant indices

You can affix the index name with the current tenant from your application using:

```ruby
class Contact < ActiveRecord::Base
include AlgoliaSearch

algoliasearch per_tenant: Proc.new { Apartment.current_tenant } do
# index name will be "#{Apartment.current_tenant}_Contact"
attribute :first_name, :last_name, :email
end
end
```

## Custom attribute definition

You can use a block to specify a complex attribute value
Expand Down
3 changes: 2 additions & 1 deletion lib/algoliasearch-rails.rb
Original file line number Diff line number Diff line change
Expand Up @@ -706,7 +706,8 @@ def algolia_index(name = nil)
def algolia_index_name(options = nil)
options ||= algoliasearch_options
name = options[:index_name] || model_name.to_s.gsub('::', '_')
name = "#{name}_#{Rails.env.to_s}" if options[:per_environment]
name = [name, Rails.env.to_s].join("_") if options[:per_environment]
name = [options[:per_tenant].call, name].join("_") if options[:per_tenant]
name
end

Expand Down
119 changes: 119 additions & 0 deletions spec/integration_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,12 @@
t.boolean :premium
t.boolean :released
end
create_table :multi_tenanted_books do |t|
t.string :name
t.string :author
t.boolean :premium
t.boolean :released
end
create_table :disabled_booleans do |t|
t.string :name
end
Expand Down Expand Up @@ -363,6 +369,32 @@ def public?
end
end

class MultiTenantedBook < ActiveRecord::Base
include AlgoliaSearch

algoliasearch :synchronous => true, :index_name => safe_index_name("SecuredBook"),
:per_environment => true, :sanitize => true, per_tenant: Proc.new { "amazon" } do

attributesToIndex [:name]
tags do
[premium ? 'premium' : 'standard', released ? 'public' : 'private']
end

add_index safe_index_name('BookAuthor'), :per_environment => true, per_tenant: Proc.new { "amazon" } do
attributesToIndex [:author]
end

add_index safe_index_name('Book'), :per_environment => true, per_tenant: Proc.new { "amazon" }, :if => :public? do
attributesToIndex [:name]
end
end

private
def public?
released && !premium
end
end

class EncodedString < ActiveRecord::Base
include AlgoliaSearch

Expand Down Expand Up @@ -1265,3 +1297,90 @@ class ReplicaThenSlave
}.to raise_error(ArgumentError)
end
end

describe 'Per-Tenant options' do
after(:each) do
MultiTenantedBook.algoliasearch_options[:per_tenant] = Proc.new { "amazon" }
end

it "should preffix the index with the evaluated Proc" do
# First tenant is amazon
expect(MultiTenantedBook.index_name).to eq("amazon_SecuredBook_development")
# Next tenant is apple
MultiTenantedBook.algoliasearch_options[:per_tenant] = Proc.new { "apple" }
expect(MultiTenantedBook.index_name).to eq("apple_SecuredBook_development")
end
end

describe 'MultiTenantedBook' do
before(:all) do
MultiTenantedBook.clear_index!(true)
MultiTenantedBook.index(safe_index_name('BookAuthor')).clear
MultiTenantedBook.index(safe_index_name('Book')).clear
end

it "should index the book in 2 indexes of 3" do
@steve_jobs = MultiTenantedBook.create! :name => 'Steve Jobs', :author => 'Walter Isaacson', :premium => true, :released => true
results = MultiTenantedBook.search('steve')
expect(results.size).to eq(1)
results.should include(@steve_jobs)

index_author = MultiTenantedBook.index(safe_index_name('BookAuthor'))
index_author.should_not be_nil
results = index_author.search('steve')
results['hits'].length.should eq(0)
results = index_author.search('walter')
results['hits'].length.should eq(1)

# premium -> not part of the public index
index_book = MultiTenantedBook.index(safe_index_name('Book'))
index_book.should_not be_nil
results = index_book.search('steve')
results['hits'].length.should eq(0)
end

it "should sanitize attributes" do
@hack = MultiTenantedBook.create! :name => "\"><img src=x onerror=alert(1)> hack0r", :author => "<script type=\"text/javascript\">alert(1)</script>", :premium => true, :released => true
b = MultiTenantedBook.raw_search('hack')
expect(b['hits'].length).to eq(1)
begin
expect(b['hits'][0]['name']).to eq('"> hack0r')
expect(b['hits'][0]['author']).to eq('alert(1)')
expect(b['hits'][0]['_highlightResult']['name']['value']).to eq('"> <em>hack</em>0r')
rescue
# rails 4.2's sanitizer
begin
expect(b['hits'][0]['name']).to eq('&quot;&gt; hack0r')
expect(b['hits'][0]['author']).to eq('')
expect(b['hits'][0]['_highlightResult']['name']['value']).to eq('&quot;&gt; <em>hack</em>0r')
rescue
# jruby
expect(b['hits'][0]['name']).to eq('"&gt; hack0r')
expect(b['hits'][0]['author']).to eq('')
expect(b['hits'][0]['_highlightResult']['name']['value']).to eq('"&gt; <em>hack</em>0r')
end
end
end

it "should handle removal in an extra index" do
# add a new public book which (not premium but released)
book = MultiTenantedBook.create! :name => 'Public book', :author => 'me', :premium => false, :released => true

# should be searchable in the 'Book' index
index = MultiTenantedBook.index(safe_index_name('Book'))
results = index.search('Public book')
expect(results['hits'].size).to eq(1)

# update the book and make it non-public anymore (not premium, not released)
book.update_attributes :released => false

# should be removed from the index
results = index.search('Public book')
expect(results['hits'].size).to eq(0)
end

it "should use the per_environment option in the additional index as well" do
index = MultiTenantedBook.index(safe_index_name('Book'))
expect(index.name).to eq("#{MultiTenantedBook.algoliasearch_options[:per_tenant].call}_#{safe_index_name('Book')}_#{Rails.env}")
end
end
7 changes: 7 additions & 0 deletions spec/spec_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,13 @@
example.run
}
end

c.after(:suite) do
indices = Algolia.client.list_indexes()['items'].sort_by { |index| index["primary"] || "" }
indices.each do |index|
Algolia.client.delete_index!(index['name'])
end
end
end

# avoid concurrent access to the same index
Expand Down