README.md

# EctoTurbo

A rich Ecto component for searching, sorting, and paginating queries.

EctoTurbo is a consolidated fork of [turbo_ecto](https://github.com/zven21/turbo_ecto), with production-tested bug fixes, security improvements, and modern Elixir compatibility.

## Installation

Add `ecto_turbo` to your list of dependencies in `mix.exs`:

```elixir
def deps do
  [
    {:ecto_turbo, "~> 0.1.0"}
  ]
end
```

## Configuration

```elixir
# config/config.exs
config :ecto_turbo, EctoTurbo,
  repo: MyApp.Repo,
  per_page: 10,
  entry_name: "data",
  paginate_name: "pagination"
```

## Usage

### Basic Query with Pagination

```elixir
# Returns %{data: [...], pagination: %{...}}
EctoTurbo.turbo(Post, %{"page" => 1, "per_page" => 20})

# Pagination payload includes:
# %{
#   current_page: 1,
#   current_pages: [1, 2, 3, "...", 20],
#   per_page: 20,
#   total_count: 100,
#   total_pages: 5,
#   next_page: 2,
#   prev_page: nil
# }
```

### Search

Use the `q` or `filter` parameter to search:

```elixir
# Exact match
EctoTurbo.turbo(Post, %{"q" => %{"price_eq" => 100}})

# Like search
EctoTurbo.turbo(Post, %{"q" => %{"name_like" => "elixir"}})

# Association search
EctoTurbo.turbo(Post, %{"q" => %{"category_name_like" => "tech"}})

# Combined OR search
EctoTurbo.turbo(Post, %{"q" => %{"name_or_body_like" => "elixir"}})
```

#### Supported Search Types

| Type | SQL | Example |
|------|-----|---------|
| `eq` | `col = 'value'` | `price_eq` |
| `not_eq` | `col != 'value'` | `price_not_eq` |
| `lt` | `col < value` | `price_lt` |
| `lteq` | `col <= value` | `price_lteq` |
| `gt` | `col > value` | `price_gt` |
| `gteq` | `col >= value` | `price_gteq` |
| `like` | `col LIKE '%value%'` | `name_like` |
| `not_like` | `col NOT LIKE '%value%'` | `name_not_like` |
| `ilike` | `col ILIKE '%value%'` | `name_ilike` |
| `not_ilike` | `col NOT ILIKE '%value%'` | `name_not_ilike` |
| `in` | `col IN (values)` | `price_in` |
| `not_in` | `col NOT IN (values)` | `price_not_in` |
| `start_with` | `col ILIKE 'value%'` | `name_start_with` |
| `not_start_with` | `col NOT ILIKE 'value%'` | `name_not_start_with` |
| `end_with` | `col ILIKE '%value'` | `name_end_with` |
| `not_end_with` | `col NOT ILIKE '%value'` | `name_not_end_with` |
| `between` | `begin < col AND col < end` | `price_between` |
| `is_true` | `col = true` | `available_is_true` |
| `is_false` | `col = false` | `available_is_false` |
| `is_null` | `col IS NULL` | `name_is_null` |
| `is_present` | `col IS NOT NULL AND col != ''` | `name_is_present` |
| `is_blank` | `col IS NULL OR col = ''` | `name_is_blank` |

### Sort

```elixir
# Single sort
EctoTurbo.turbo(Post, %{"s" => "updated_at+desc"})

# Or using "sort" key
EctoTurbo.turbo(Post, %{"sort" => "price+asc"})

# Multiple sorts
EctoTurbo.turbo(Post, %{"s" => ["updated_at+desc", "name+asc"]})
```

### Options

```elixir
EctoTurbo.turbo(Post, params,
  repo: MyApp.Repo,          # Override configured repo
  per_page: 25,               # Override per_page
  entry_name: "entries",       # Custom key for results
  paginate_name: "meta",       # Custom key for pagination
  with_paginate: false,        # Return flat list without pagination
  prefix: "my_schema",         # Ecto query prefix
  callback: fn q -> q end      # Transform query before execution
)
```

### Query Builder Only

Use `turboq/2` to build the query without executing it:

```elixir
query = EctoTurbo.turboq(Post, %{"q" => %{"name_like" => "elixir"}, "s" => "name+asc"})
# Returns an Ecto.Query struct
```

## Improvements over turbo_ecto

- Safer atom conversion using `String.to_existing_atom/1` to prevent atom table exhaustion
- Fixed `end_with` / `not_end_with` search types (was incorrectly using `%value%` instead of `%value`)
- Compatible with `ecto_sql ~> 3.11` (OrderBy `:append` parameter)
- Modern Elixir syntax (`~c` sigils, `not in` operator)
- Compile-time warnings as errors

## License

MIT - See [LICENSE.md](LICENSE.md) for details.

Originally created by [Zven Wang](https://github.com/zven21).