# Ecto.Rescope
![travis ci badge](https://travis-ci.org/erikreedstrom/ecto_rescope.svg?branch=master)
Extends Ecto to allow rescoping of the default schema query.
A typical usecase for this functionality is excluding soft-deleted records by default.
## Usage
The most basic example for rescoping is to add directly to a schema with an inline function.
```elixir
defmodule User do
use Ecto.Schema
import Ecto.Query, only: [from: 2]
import Ecto.Rescope
schema "users" do
field(:is_deleted, :boolean)
end
rescope(fn query ->
from(q in query, where: q.is_deleted == false)
end)
end
```
In a more abstracted example, we might move the logic to a separate module for reuse.
```elixir
defmodule SoftDelete do
import Ecto.Query, only: [from: 2]
def exclude_deleted(query) do
from(q in query, where: q.is_deleted == false)
end
end
defmodule User do
use Ecto.Schema
import Ecto.Rescope
schema "users" do
field(:is_deleted, :boolean)
end
rescope(&SoftDelete.exclude_deleted/1)
end
```
When we want to query without the default scoping applied, we can do so with `unscoped/0`,
which is added by the `rescope/1` macro.
```elixir
User.unscoped()
```
## Caveats
### Using with `Ecto.Query` API
When using `from` or `join` macros from the `Ecto.Query` api, the query builder defines the
queryable source at runtime, thus ignoring `__schema__(:query)` and the associated override.
Because of this, queries such as the following will not work as expected:
```elixir
from(q in User, join: t in Team, on: t.id == q.team_id)
# SELECT u0."id", u0."name", u0."is_deleted", u0."team_id" FROM "users" AS u0 INNER JOIN "teams" AS t1 ON
# t1."id" = u0."team_id" []
```
Note the lack of `(u0."is_deleted" = FALSE)` or `(t1."is_deleted" = FALSE)` in the associated SQL log.
However, this can be worked around using the `scoped/0` function that is defined by the `rescope/0` macro.
```elixir
from(q in User.scoped(), join: t in ^Team.scoped(), on: t.id == q.team_id)
# SELECT u0."id", u0."name", u0."is_deleted", u0."team_id" FROM "users" AS u0 INNER JOIN "teams" AS t1 ON
# (t1."is_deleted" = FALSE) AND (t1."id" = u0."team_id") WHERE (u0."is_deleted" = FALSE) []
```
> NOTE: Although this can seem a bit cumbersome, when using query building libraries such as DockYard's
> [Inquisitor](https://github.com/DockYard/inquisitor), the problem is avoided as the queryable module is converted
> to a query struct prior to being used within the `from` macro.
### Private API
This library overrides private reflection functions defined on schemas by the `ecto` library, specifically
`__schema__(:query)` ([source](https://github.com/elixir-ecto/ecto/blob/master/lib/ecto/schema.ex#L548-L555)).
While this technique has been used stable in production for multiple years, there is no guarantee `ecto`
won't change the underlying functionality at some point in the future.
## Installation
The package can be installed by adding `ecto_rescope` to your list of dependencies in `mix.exs`:
```elixir
def deps do
[
{:ecto_rescope, "~> 0.1.0"}
]
end
```
Documentation can be found at [https://hexdocs.pm/ecto_rescope](https://hexdocs.pm/ecto_rescope).
## License
The source code is under the Apache 2 License.
Copyright (c) 2019 Erik Reedstrom
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
for the specific language governing permissions and limitations under the License.