Skip to content

Commit

Permalink
Merge pull request #25 from dreamingechoes/add-multi-tenancy-capabili…
Browse files Browse the repository at this point in the history
…ties-with-ecto-meta-prefix

Add multi tenancy capabilities with ecto meta prefix
  • Loading branch information
izelnakri committed Aug 12, 2017
2 parents 968a0b0 + 1359026 commit 6fec5cf
Show file tree
Hide file tree
Showing 18 changed files with 1,799 additions and 215 deletions.
53 changes: 53 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,58 @@ Bang functions assume the operation will always be successful, otherwise functio
# "updated_at" => "2016-09-15T22:00:59"},
# item_id: 1, item_type: "Post", originator_id: nil, originator: nil, meta: nil}
```

## Working with multi tenancy

Sometimes you have to deal with applications where you need multi tenancy capabilities,
and you want to keep tracking of the versions of your data on different schemas (PostgreSQL)
or databases (MySQL).

You can use the [Ecto.Query prefix](https://hexdocs.pm/ecto/Ecto.Query.html#module-query-prefix)
in order to switch between different schemas/databases for your own data, so
you can specify in your changeset where to store your record. Example:

```elixir
tenant = "tenant_id"
changeset = User.changeset(%User{}, %{first_name: "Izel", last_name: "Nakri"})

changeset
|> Ecto.Queryable.to_query()
|> Map.put(:prefix, tenant)
|> Repo.insert()
```

PaperTrail also allows you to store the `Version` entries generated by your activity in
different schemas/databases by using the value of the element `:prefix` on the options
of the functions. Example:

```elixir
tenant = "tenant_id"

changeset =
User.changeset(%User{}, %{first_name: "Izel", last_name: "Nakri"})
|> Ecto.Queryable.to_query()
|> Map.put(:prefix, tenant)

PaperTrail.insert(changeset, [prefix: tenant])
```

By doing this, you're storing the new `User` entry into the schema/database
specified by the `:prefix` value (`tenant_id`). Notice that the `User`'s changeset it's sent
with the `:prefix` already set properly, so PaperTrail **only will take care of the
storage of the generated `Version` entry in the desired schema/database**, be sure
to add this info to your changeset before the execution of the PaperTrail function.

PaperTrail can get versions of records or models from different schemas/databases as well
by using the value of the element `:prefix` on the options of the functions. Example:

```elixir
tenant = "tenant_id"
id = 1

PaperTrail.get_versions(User, id, [prefix: tenant])
```

## Suggestions
- PaperTrail.Version(s) order matter,
- don't delete your paper_trail versions, instead you can merge them
Expand All @@ -392,6 +444,7 @@ Many thanks to:
- [Jason Draper](https://github.com/drapergeek) - UUID primary keys feature
- [Josh Taylor](https://github.com/joshuataylor) - Maintenance and new feature suggestions
- [Mitchell Henke](https://github.com/mitchellhenke) - Fixed weird elixir compiler warnings
- [Iván González](https://github.com/dreamingechoes) - Multi tenancy feature and some minor refactors
- [Izel Nakri](https://github.com/izelnakri) - The Originator of this library. See what I did there ;)

Additional thanks to:
Expand Down
100 changes: 50 additions & 50 deletions example/test/company_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -12,24 +12,23 @@ defmodule CompanyTest do
end

test "creating a company creates a company version with correct attributes" do
new_company = Company.changeset(%Company{}, %{
name: "Acme LLC", is_active: true, city: "Greenwich", people: []
})

{:ok, result} = PaperTrail.insert(new_company, origin: "test")

company_count = Repo.all(
from company in Company,
select: count(company.id)
)
{:ok, result} =
%Company{}
|> Company.changeset(%{name: "Acme LLC", is_active: true, city: "Greenwich", people: []})
|> PaperTrail.insert(origin: "test")

company_count =
from(company in Company, select: count(company.id))
|> Repo.all()
version_count =
from(version in PaperTrail.Version, select: count(version.id))
|> Repo.all()
first_company =
first(Company, :id)
|> preload(:people)
|> Repo.one()

company = result[:model] |> Map.drop([:__meta__, :__struct__, :inserted_at, :updated_at, :id])

version_count = Repo.all(
from version in PaperTrail.Version,
select: count(version.id)
)

version = result[:version] |> Map.drop([:__meta__, :__struct__, :inserted_at])

assert company_count == [1]
Expand All @@ -50,7 +49,7 @@ defmodule CompanyTest do
assert Map.drop(version, [:id]) == %{
event: "insert",
item_type: "Company",
item_id: Repo.one(first(Company, :id)).id,
item_id: first_company.id,
item_changes: Map.drop(result[:model], [:__meta__, :__struct__, :people]),
origin: "test",
originator_id: nil,
Expand All @@ -59,27 +58,27 @@ defmodule CompanyTest do
end

test "updating a company creates a company version with correct item_changes" do
old_company = first(Company, :id) |> preload(:people) |> Repo.one
new_company = Company.changeset(old_company, %{
city: "Hong Kong",
website: "http://www.acme.com",
facebook: "acme.llc"
})

{:ok, result} = PaperTrail.update(new_company)
first_company =
first(Company, :id)
|> preload(:people)
|> Repo.one()

{:ok, result} =
first_company
|> Company.changeset(%{
city: "Hong Kong",
website: "http://www.acme.com",
facebook: "acme.llc"
}) |> PaperTrail.update()

company_count = Repo.all(
from company in Company,
select: count(company.id)
)
company_count =
from(company in Company, select: count(company.id))
|> Repo.all()
version_count =
from(version in PaperTrail.Version, select: count(version.id))
|> Repo.all()

company = result[:model] |> Map.drop([:__meta__, :__struct__, :inserted_at, :updated_at, :id])

version_count = Repo.all(
from version in PaperTrail.Version,
select: count(version.id)
)

version = result[:version] |> Map.drop([:__meta__, :__struct__, :inserted_at])

assert company_count == [1]
Expand All @@ -100,7 +99,7 @@ defmodule CompanyTest do
assert Map.drop(version, [:id]) == %{
event: "update",
item_type: "Company",
item_id: Repo.one(first(Company, :id)).id,
item_id: first_company.id,
item_changes: %{city: "Hong Kong", website: "http://www.acme.com", facebook: "acme.llc"},
origin: nil,
originator_id: nil,
Expand All @@ -109,22 +108,23 @@ defmodule CompanyTest do
end

test "deleting a company creates a company version with correct attributes" do
company = first(Company, :id) |> preload(:people) |> Repo.one

{:ok, result} = PaperTrail.delete(company)

company_count = Repo.all(
from company in Company,
select: count(company.id)
)
company =
first(Company, :id)
|> preload(:people)
|> Repo.one()

{:ok, result} =
company
|> PaperTrail.delete()

company_count =
from(company in Company, select: count(company.id))
|> Repo.all()
version_count =
from(version in PaperTrail.Version, select: count(version.id))
|> Repo.all()

company_ref = result[:model] |> Map.drop([:__meta__, :__struct__, :inserted_at, :updated_at, :id])

version_count = Repo.all(
from version in PaperTrail.Version,
select: count(version.id)
)

version = result[:version] |> Map.drop([:__meta__, :__struct__, :inserted_at])

assert company_count == [0]
Expand Down
187 changes: 187 additions & 0 deletions example/test/multi_tenant_company_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
defmodule MultiTenantCompanyTest do
use ExUnit.Case
import Ecto.Query

setup_all do
MultiTenantHelper.setup_tenant(Repo)
:ok
end

test "[multi tenant] creating a company creates a company version with correct attributes" do
{:ok, result} =
%Company{}
|> Company.changeset(%{name: "Acme LLC", is_active: true, city: "Greenwich", people: []})
|> MultiTenantHelper.add_prefix_to_changeset()
|> PaperTrail.insert(origin: "test", prefix: MultiTenantHelper.tenant())

company_count =
from(company in Company, select: count(company.id))
|> MultiTenantHelper.add_prefix_to_query()
|> Repo.all()
version_count =
from(version in PaperTrail.Version, select: count(version.id))
|> MultiTenantHelper.add_prefix_to_query()
|> Repo.all()
regular_version_count =
from(version in PaperTrail.Version, select: count(version.id))
|> Repo.all()
first_company =
first(Company, :id)
|> preload(:people)
|> MultiTenantHelper.add_prefix_to_query()
|> Repo.one()

company = result[:model] |> Map.drop([:__meta__, :__struct__, :inserted_at, :updated_at, :id])
version = result[:version] |> Map.drop([:__meta__, :__struct__, :inserted_at])

assert company_count == [1]
assert version_count == [1]
assert regular_version_count == [0]

assert company == %{
name: "Acme LLC",
is_active: true,
city: "Greenwich",
website: nil,
address: nil,
facebook: nil,
twitter: nil,
founded_in: nil,
people: []
}

assert Map.drop(version, [:id]) == %{
event: "insert",
item_type: "Company",
item_id: first_company.id,
item_changes: Map.drop(result[:model], [:__meta__, :__struct__, :people]),
origin: "test",
originator_id: nil,
meta: nil
}
end

test "[multi tenant] updating a company creates a company version with correct item_changes" do
first_company =
first(Company, :id)
|> preload(:people)
|> MultiTenantHelper.add_prefix_to_query()
|> Repo.one()

{:ok, result} =
first_company
|> Company.changeset(%{
city: "Hong Kong",
website: "http://www.acme.com",
facebook: "acme.llc"
})
|> MultiTenantHelper.add_prefix_to_changeset()
|> PaperTrail.update(prefix: MultiTenantHelper.tenant())

company_count =
from(company in Company, select: count(company.id))
|> MultiTenantHelper.add_prefix_to_query()
|> Repo.all()
version_count =
from(version in PaperTrail.Version, select: count(version.id))
|> MultiTenantHelper.add_prefix_to_query()
|> Repo.all()
regular_version_count =
from(version in PaperTrail.Version, select: count(version.id))
|> Repo.all()

company = result[:model] |> Map.drop([:__meta__, :__struct__, :inserted_at, :updated_at, :id])
version = result[:version] |> Map.drop([:__meta__, :__struct__, :inserted_at])

assert company_count == [1]
assert version_count == [2]
assert regular_version_count == [0]

assert company == %{
name: "Acme LLC",
is_active: true,
city: "Hong Kong",
website: "http://www.acme.com",
address: nil,
facebook: "acme.llc",
twitter: nil,
founded_in: nil,
people: []
}

assert Map.drop(version, [:id]) == %{
event: "update",
item_type: "Company",
item_id: first_company.id,
item_changes: %{city: "Hong Kong", website: "http://www.acme.com", facebook: "acme.llc"},
origin: nil,
originator_id: nil,
meta: nil
}
end

test "[multi tenant] deleting a company creates a company version with correct attributes" do
company =
first(Company, :id)
|> preload(:people)
|> MultiTenantHelper.add_prefix_to_query()
|> Repo.one()

{:ok, result} =
company
|> PaperTrail.delete(prefix: MultiTenantHelper.tenant())

company_count =
from(company in Company, select: count(company.id))
|> MultiTenantHelper.add_prefix_to_query()
|> Repo.all()
version_count =
from(version in PaperTrail.Version, select: count(version.id))
|> MultiTenantHelper.add_prefix_to_query()
|> Repo.all()
regular_version_count =
from(version in PaperTrail.Version, select: count(version.id))
|> Repo.all()

company_ref = result[:model] |> Map.drop([:__meta__, :__struct__, :inserted_at, :updated_at, :id])
version = result[:version] |> Map.drop([:__meta__, :__struct__, :inserted_at])

assert company_count == [0]
assert version_count == [3]
assert regular_version_count == [0]

assert company_ref == %{
name: "Acme LLC",
is_active: true,
city: "Hong Kong",
website: "http://www.acme.com",
address: nil,
facebook: "acme.llc",
twitter: nil,
founded_in: nil,
people: []
}

assert Map.drop(version, [:id]) == %{
event: "delete",
item_type: "Company",
item_id: company.id,
item_changes: %{
id: company.id,
inserted_at: company.inserted_at,
updated_at: company.updated_at,
name: "Acme LLC",
is_active: true,
website: "http://www.acme.com",
city: "Hong Kong",
address: nil,
facebook: "acme.llc",
twitter: nil,
founded_in: nil
},
origin: nil,
originator_id: nil,
meta: nil
}
end
end
Loading

0 comments on commit 6fec5cf

Please sign in to comment.