README.md

# 🔄 FlopRest

REST-friendly query parameters for [Flop](https://github.com/woylie/flop).

- **🎯 Stripe-Style Parameters**: Intuitive `field[operator]=value` syntax that API consumers expect
- **🔀 All Pagination Types**: Cursor-based, page-based, and offset-based pagination
- **🔗 Pagination Links**: Generate `next`/`prev` URLs from Flop metadata with `build_path/2`
- **âš¡ Frontend-Ready**: Works naturally with URLSearchParams, axios, TanStack Query, and more

[![Hex.pm](https://img.shields.io/hexpm/v/flop_rest.svg)](https://hex.pm/packages/flop_rest)
[![Documentation](https://img.shields.io/badge/docs-hexdocs-blue.svg)](https://hexdocs.pm/flop_rest)
[![CI](https://github.com/guess/flop_rest/actions/workflows/ci.yml/badge.svg)](https://github.com/guess/flop_rest/actions/workflows/ci.yml)
[![License](https://img.shields.io/badge/license-MIT-green.svg)](https://github.com/guess/flop_rest/blob/main/LICENSE)

<div align="center">
    <img src="https://github.com/guess/flop_rest/raw/main/docs/flop_rest.png" alt="FlopRest" width="250">
</div>

## The Problem

Flop is excellent for filtering, sorting, and paginating Ecto queries. But its query parameter format isn't ideal for API consumers:

```
GET /events
  ?filters[0][field]=status
  &filters[0][op]==
  &filters[0][value]=published
  &filters[1][field]=starts_at
  &filters[1][op]=>=
  &filters[1][value]=2024-01-01
  &order_by[0]=starts_at
  &order_directions[0]=desc
  &first=20
```

This is verbose, error-prone, and unfamiliar to developers used to modern REST APIs.

## The Solution

FlopRest transforms intuitive, Stripe-style query parameters into Flop format:

```
GET /events?status=published&starts_at[gte]=2024-01-01&sort=-starts_at&limit=20
```

Same query. Same Flop power underneath. Better developer experience on top.

## When to Use FlopRest

**Building a Phoenix HTML/LiveView app?** Use [Flop Phoenix](https://hexdocs.pm/flop_phoenix) — it provides UI components for pagination, tables, and filters.

**Building a JSON API?** Use FlopRest — it transforms REST-style query parameters that frontend developers expect.

## Frontend Integration

Standard JavaScript works out of the box:

```javascript
// No special serialization needed
const params = new URLSearchParams({
  status: "published",
  "created_at[gte]": "2024-01-01",
  sort: "-created_at",
  limit: "20",
});

fetch(`/api/events?${params}`);
```

Works naturally with TanStack Query, SWR, RTK Query, axios, or any HTTP client.

## Installation

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

```elixir
def deps do
  [
    {:flop_rest, "~> 0.4"},
    {:flop, "~> 0.26"}
  ]
end
```

## Usage

Transform incoming REST parameters to Flop format:

```elixir
def index(conn, params) do
  flop_params = FlopRest.normalize(params)

  with {:ok, {events, meta}} <- Flop.validate_and_run(Event, flop_params, for: Event) do
    json(conn, %{data: events})
  end
end
```

### Schema-Aware Filtering

Pass the `:for` option to restrict filters to your schema's `filterable` fields. Non-filterable params are kept in the result at the root level for your own handling:

```elixir
# Given a schema with filterable: [:name, :status]
FlopRest.normalize(%{"name" => "Fido", "custom_field" => "value"}, for: Pet)
# => %{
#   "filters" => [%{"field" => "name", "op" => "==", "value" => "Fido"}],
#   "custom_field" => "value"
# }
```

This matches Flop's API conventions and lets you safely pass user params while keeping non-filter data accessible:

```elixir
def index(conn, params) do
  flop_params = FlopRest.normalize(params, for: Pet)

  with {:ok, {pets, meta}} <- Flop.validate_and_run(Pet, flop_params, for: Pet) do
    json(conn, %{data: pets})
  end
end
```

### Building Pagination Links

Use `build_path/2` to generate pagination links for API responses:

```elixir
def index(conn, params) do
  flop_params = FlopRest.normalize(params)

  with {:ok, {events, meta}} <- Flop.validate_and_run(Event, flop_params, for: Event) do
    json(conn, %{
      data: events,
      links: %{
        self: FlopRest.build_path(conn.request_path, meta.flop),
        next: meta.has_next_page? && FlopRest.build_path(conn.request_path, meta.next_flop),
        prev: meta.has_previous_page? && FlopRest.build_path(conn.request_path, meta.previous_flop)
      }
    })
  end
end
```

This produces links like:

```json
{
  "data": [...],
  "links": {
    "self": "/events?limit=20&starting_after=abc123",
    "next": "/events?limit=20&starting_after=xyz789",
    "prev": "/events?limit=20&ending_before=abc123"
  }
}
```

Use `to_query/1` if you need the raw map to merge with other parameters:

```elixir
query = FlopRest.to_query(meta.next_flop)
# => %{"limit" => 20, "starting_after" => "xyz789"}
```

Both functions accept `Flop.t()` or `Flop.Meta.t()` structs.

## Filters

Bare values become equality filters:

```
status=published     →  %{field: "status", op: "==", value: "published"}
```

Operators are specified as nested keys:

```
amount[gte]=100      →  %{field: "amount", op: ">=", value: "100"}
amount[lt]=500       →  %{field: "amount", op: "<", value: "500"}
```

Multiple operators on the same field create multiple filters:

```
amount[gte]=100&amount[lt]=500  →  two separate filters
```

List values for `in` and `not_in`:

```
status[in][]=draft&status[in][]=review  →  %{field: "status", op: "in", value: ["draft", "review"]}
```

### Operator Reference

| REST Operator  | Flop Operator  | Description                    |
| -------------- | -------------- | ------------------------------ |
| `eq`           | `==`           | Equal (also bare value)        |
| `ne`           | `!=`           | Not equal                      |
| `lt`           | `<`            | Less than                      |
| `lte`          | `<=`           | Less than or equal             |
| `gt`           | `>`            | Greater than                   |
| `gte`          | `>=`           | Greater than or equal          |
| `in`           | `in`           | In list                        |
| `not_in`       | `not_in`       | Not in list                    |
| `contains`     | `contains`     | Array contains                 |
| `not_contains` | `not_contains` | Array does not contain         |
| `like`         | `like`         | SQL LIKE                       |
| `not_like`     | `not_like`     | SQL NOT LIKE                   |
| `like_and`     | `like_and`     | LIKE with AND                  |
| `like_or`      | `like_or`      | LIKE with OR                   |
| `ilike`        | `ilike`        | Case-insensitive LIKE          |
| `not_ilike`    | `not_ilike`    | Case-insensitive NOT LIKE      |
| `ilike_and`    | `ilike_and`    | Case-insensitive LIKE with AND |
| `ilike_or`     | `ilike_or`     | Case-insensitive LIKE with OR  |
| `empty`        | `empty`        | Is NULL                        |
| `not_empty`    | `not_empty`    | Is NOT NULL                    |
| `search`       | `=~`           | Search (configurable in Flop)  |

Unknown operators are passed through for Flop to validate.

## Sorting

Use `-` prefix for descending, `+` or no prefix for ascending:

```
sort=name              →  order_by: ["name"], order_directions: ["asc"]
sort=-created_at       →  order_by: ["created_at"], order_directions: ["desc"]
sort=-created_at,name  →  order_by: ["created_at", "name"], order_directions: ["desc", "asc"]
```

## Pagination

FlopRest supports all three Flop pagination types.

### Cursor-based (Stripe-style)

```
limit=20                        →  first: 20
limit=20&starting_after=abc123  →  first: 20, after: "abc123"
limit=20&ending_before=xyz789   →  last: 20, before: "xyz789"
```

### Page-based

```
page=2&page_size=25  →  page: 2, page_size: 25
```

### Offset-based

```
offset=50&limit=25  →  offset: 50, limit: 25
```

## Design Philosophy

FlopRest is a **pure transformation layer**. It does not validate parameters - that's Flop's job. Invalid operators or conflicting pagination params are passed through, and Flop will return appropriate errors.

This keeps FlopRest simple and ensures Flop remains the single source of truth for validation rules.

## License

MIT License. See [LICENSE](LICENSE) for details.