View Source Flop (Flop v0.17.0)
Flop is a helper library for filtering, ordering and pagination with Ecto.
usage
Usage
The simplest way of using this library is just to use
Flop.validate_and_run/3
and Flop.validate_and_run!/3
. Both functions
take a queryable and a parameter map, validate the parameters, run the query
and return the query results and the meta information.
iex> Flop.Repo.insert_all(Flop.Pet, [
...> %{name: "Harry", age: 4, species: "C. lupus"},
...> %{name: "Maggie", age: 1, species: "O. cuniculus"},
...> %{name: "Patty", age: 2, species: "C. aegagrus"}
...> ])
iex> params = %{order_by: ["name", "age"], page: 1, page_size: 2}
iex> {:ok, {results, meta}} =
...> Flop.validate_and_run(
...> Flop.Pet,
...> params,
...> repo: Flop.Repo
...> )
iex> Enum.map(results, & &1.name)
["Harry", "Maggie"]
iex> meta.total_count
3
iex> meta.total_pages
2
iex> meta.has_next_page?
true
Under the hood, these functions just call Flop.validate/2
and Flop.run/3
,
which in turn calls Flop.all/3
and Flop.meta/3
. If you need finer control
about if and when to execute each step, you can call those functions directly.
See Flop.Meta
for descriptions of the meta fields.
global-configuration
Global configuration
You can set some global options like the default Ecto repo via the application
environment. All global options can be overridden by passing them directly to
the functions or configuring the options for a schema module via
Flop.Schema
.
import Config
config :flop, repo: MyApp.Repo
See Flop.option/0
for a description of all available options.
config-modules
Config modules
Instead of setting global options in the application environment, you can also create a Flop config module. This is especially useful in an umbrella application, or if you have multiple Repos.
defmodule MyApp.Flop do
use Flop, repo: MyApp.Repo, default_limit: 25
end
This will define wrapper functions around all Flop
functions that take a
query, Flop parameters and options:
Flop.all/3
Flop.count/3
Flop.filter/3
Flop.meta/3
Flop.order_by/3
Flop.paginate/3
Flop.query/3
Flop.run/3
Flop.validate_and_run/3
Flop.validate_and_run!/3
So instead of using Flop.validate_and_run/3
, you would call
MyApp.Flop.validate_and_run/3
.
If you have both a config module and a global application config, Flop will fall back to the application config if an option is not set.
See Flop.option/0
for a description of all available options.
schema-options
Schema options
You can set some options for a schema by deriving Flop.Schema
. The options
are evaluated at the validation step.
defmodule Pet do
use Ecto.Schema
@derive {Flop.Schema,
filterable: [:name, :species],
sortable: [:name, :age],
default_limit: 20,
max_limit: 100}
schema "pets" do
field :name, :string
field :age, :integer
field :species, :string
field :social_security_number, :string
end
end
You need to pass the schema to Flop.validate/2
or any function that
includes the validation step with the :for
option.
iex> params = %{"order_by" => ["name", "age"], "limit" => 5}
iex> {:ok, flop} = Flop.validate(params, for: Flop.Pet)
iex> flop.limit
5
iex> params = %{"order_by" => ["name", "age"], "limit" => 10_000}
iex> {:error, meta} = Flop.validate(params, for: Flop.Pet)
iex> [limit: [{msg, _}]] = meta.errors
iex> msg
"must be less than or equal to %{number}"
iex> params = %{"order_by" => ["name", "age"], "limit" => 10_000}
iex> {:error, %Flop.Meta{} = meta} =
...> Flop.validate_and_run(
...> Flop.Pet,
...> params,
...> for: Flop.Pet
...> )
iex> [limit: [{msg, _}]] = meta.errors
iex> msg
"must be less than or equal to %{number}"
ordering
Ordering
To add an ordering clause to a query, you need to set the :order_by
and
optionally the :order_directions
parameter. :order_by
should be the list
of fields, while :order_directions
is a list of Flop.order_direction/0
.
:order_by
and :order_directions
are zipped when generating the ORDER BY
clause. If no order directions are given, :asc
is used as default.
iex> params = %{
...> "order_by" => ["name", "age"],
...> "order_directions" => ["asc", "desc"]
...> }
iex> {:ok, flop} = Flop.validate(params)
iex> flop.order_by
[:name, :age]
iex> flop.order_directions
[:asc, :desc]
Flop uses these two fields instead of a keyword list, so that the order instructions can be easily passed in a query string.
pagination
Pagination
For queries using OFFSET
and LIMIT
, you have the choice between
page-based pagination parameters:
%{page: 5, page_size: 20}
and offset-based pagination parameters:
%{offset: 100, limit: 20}
For cursor-based pagination, you can either use :first
/:after
or
:last
/:before
. You also need to pass the :order_by
parameter or set a
default order for the schema via Flop.Schema
.
iex> Flop.Repo.insert_all(Flop.Pet, [
...> %{name: "Harry", age: 4, species: "C. lupus"},
...> %{name: "Maggie", age: 1, species: "O. cuniculus"},
...> %{name: "Patty", age: 2, species: "C. aegagrus"}
...> ])
iex>
iex> # forward (first/after)
iex>
iex> params = %{first: 2, order_by: [:species, :name]}
iex> {:ok, {results, meta}} = Flop.validate_and_run(Flop.Pet, params)
iex> Enum.map(results, & &1.name)
["Patty", "Harry"]
iex> meta.has_next_page?
true
iex> end_cursor = meta.end_cursor
"g3QAAAACZAAEbmFtZW0AAAAFSGFycnlkAAdzcGVjaWVzbQAAAAhDLiBsdXB1cw=="
iex> params = %{first: 2, after: end_cursor, order_by: [:species, :name]}
iex> {:ok, {results, meta}} = Flop.validate_and_run(Flop.Pet, params)
iex> Enum.map(results, & &1.name)
["Maggie"]
iex> meta.has_next_page?
false
iex>
iex> # backward (last/before)
iex>
iex> params = %{last: 2, order_by: [:species, :name]}
iex> {:ok, {results, meta}} = Flop.validate_and_run(Flop.Pet, params)
iex> Enum.map(results, & &1.name)
["Harry", "Maggie"]
iex> meta.has_previous_page?
true
iex> start_cursor = meta.start_cursor
"g3QAAAACZAAEbmFtZW0AAAAFSGFycnlkAAdzcGVjaWVzbQAAAAhDLiBsdXB1cw=="
iex> params = %{last: 2, before: start_cursor, order_by: [:species, :name]}
iex> {:ok, {results, meta}} = Flop.validate_and_run(Flop.Pet, params)
iex> Enum.map(results, & &1.name)
["Patty"]
iex> meta.has_previous_page?
false
By default, it is assumed that the query result is a list of maps or structs.
If your query returns a different data structure, you can pass the
:cursor_value_func
option to retrieve the cursor values. See
Flop.option/0
and Flop.Cursor
for more information.
You can restrict which pagination types are available. See Flop.option/0
for details.
filters
Filters
Filters can be passed as a list of maps. It is recommended to define the
filterable fields for a schema using Flop.Schema
.
iex> Flop.Repo.insert_all(Flop.Pet, [
...> %{name: "Harry", age: 4, species: "C. lupus"},
...> %{name: "Maggie", age: 1, species: "O. cuniculus"},
...> %{name: "Patty", age: 2, species: "C. aegagrus"}
...> ])
iex>
iex> params = %{filters: [%{field: :name, op: :=~, value: "Mag"}]}
iex> {:ok, {results, meta}} = Flop.validate_and_run(Flop.Pet, params)
iex> meta.total_count
1
iex> [pet] = results
iex> pet.name
"Maggie"
See Flop.Filter.op/0
for a list of all available filter operators.
graphql-and-relay
GraphQL and Relay
The parameters used for cursor-based pagination follow the Relay specification, so you can just pass the arguments you get from the client on to Flop.
Flop.Relay
can convert the query results returned by
Flop.validate_and_run/3
into Edges
and PageInfo
formats required for
Relay connections.
For example, if you have a context module like this:
defmodule MyApp.Flora
import Ecto.query, warn: false
alias MyApp.Flora.Plant
def list_plants_by_continent(%Continent{} = continent, %{} = args) do
Plant
|> where(continent_id: ^continent.id)
|> Flop.validate_and_run(args, for: Plant)
end
end
Then your Absinthe resolver for the plants
connection may look something
like this:
def list_plants(args, %{source: %Continent{} = continent}) do
with {:ok, result} <-
Flora.list_plants_by_continent(continent, args) do
{:ok, Flop.Relay.connection_from_result(result)}
end
end
Link to this section Summary
Types
Options that can be passed to most of the functions or that can be set via the application environment.
Represents the supported order direction values.
Represents the pagination type.
Represents the query parameters for filtering, ordering and pagination.
Functions
Applies the given Flop to the given queryable and returns all matchings entries.
Returns the names of the bindings that are required for the filters and order clauses of the given Flop.
Returns the total count of entries matching the filter conditions of the Flop.
Returns the current order direction for the given field.
Applies the filter
parameter of a Flop.t/0
to an Ecto.Queryable.t/0
.
Returns the option with the given key.
Converts a map of filter conditions into a list of Flop filter params.
Returns meta information for the given query and flop that can be used for building the pagination links.
Converts key/value filter parameters at the root of a map, converts them into
a list of filter parameter maps and nests them under the :filters
key.
Applies the order_by
and order_directions
parameters of a Flop.t/0
to an Ecto.Queryable.t/0
.
Applies the pagination parameters of a Flop.t/0
to an
Ecto.Queryable.t/0
.
Updates the order_by
and order_directions
values of a Flop
struct.
Adds clauses for filtering, ordering and pagination to a
Ecto.Queryable.t/0
.
Removes the after
and before
cursors from a Flop struct.
Removes all filters from a Flop struct.
Removes the order parameters from a Flop struct.
Applies the given Flop to the given queryable, retrieves the data and the meta data.
Sets the offset value of a Flop
struct while also removing/converting
pagination parameters for other pagination types.
Sets the page value of a Flop
struct while also removing/converting
pagination parameters for other pagination types.
Sets the offset of a Flop struct to the next page depending on the limit.
Sets the page of a Flop struct to the next page.
Sets the offset of a Flop struct to the page depending on the limit.
Sets the page of a Flop struct to the previous page, but not less than 1.
Same as Flop.validate/2
, but raises an Ecto.InvalidChangesetError
if the
parameters are invalid.
Validates a Flop.t/0
.
Same as Flop.validate_and_run/3
, but raises on error.
Validates the given flop parameters and retrieves the data and meta data on success.
Link to this section Types
@type option() :: {:cursor_value_func, (any(), [atom()] -> map())} | {:default_limit, pos_integer()} | {:default_order, %{ :order_by => [atom()], optional(:order_directions) => [order_direction()] }} | {:default_pagination_type, pagination_type()} | {:filtering, boolean()} | {:for, module()} | {:max_limit, pos_integer()} | {:order_query, Ecto.Queryable.t()} | {:ordering, boolean()} | {:pagination, boolean()} | {:pagination_types, [pagination_type()]} | {:repo, module()} | {:query_opts, Keyword.t()}
Options that can be passed to most of the functions or that can be set via the application environment.
:cursor_value_func
- 2-arity function used to get the (unencoded) cursor value from a record. Only used with cursor-based pagination. The first argument is the record, the second argument is the list of fields used in theORDER BY
clause. Needs to return a map with the order fields as keys and the the record values of these fields as values. Defaults toFlop.Cursor.get_cursor_from_node/2
.:default_limit
- Sets a global default limit for queries that is used if no default limit is set for a schema and no limit is set in the parameters. Can only be set in the application configuration.:default_order
- Sets the default order for a query if none is passed in the parameters or if ordering is disabled. Can be set in the schema or in the options passed to the query functions.:default_pagination_type
- The pagination type to use when setting default parameters and the pagination type cannot be determined from the parameters. Parameters for other pagination types can still be passed when setting this option. To restrict which pagination types can be used, set the:pagination_types
option.:filtering
(boolean) - Can be set tofalse
to silently ignore filter parameters.:for
- The schema module to be used for validation.Flop.Schema
must be derived for the given module. This option is optional and can not be set globally. If it is not set, schema specific validation will be omitted. Used by the validation functions. It is also used to determine which fields are join and compound fields.:max_limit
- Sets a global maximum limit for queries that is used if no maximum limit is set for a schema. Can only be set in the application configuration.:order_query
- Allows you to set a separate base query for counting. Can only be passed as an option to one of the query functions. SeeFlop.validate_and_run/3
andFlop.count/3
.:pagination
(boolean) - Can be set tofalse
to silently ignore pagination parameters.:pagination_types
- Defines which pagination types are allowed. Parameters for other pagination types will not be cast. By default, all pagination types are allowed. See alsoFlop.pagination_type/0
.:query_opts
- These options are passed to theEcto.Repo
query functions. See the Ecto documentation forEcto.Repo.all/2
,Ecto.Repo.aggregate/3
, and the "Shared Options" section ofEcto.Repo
.:ordering
(boolean) - Can be set tofalse
to silently ignore order parameters. Default orders are still applied.:repo
- The Ecto Repo module to use for the database query. Used by all functions that execute a database query.
All options can be passed directly to the functions. Some of the options can
be set on a schema level via Flop.Schema
.
All options except :for
, :default_order
and :count_query
can be set
globally via the application environment.
import Config
config :flop,
default_limit: 25,
filtering: false,
cursor_value_func: &MyApp.Repo.get_cursor_value/2,
max_limit: 100,
ordering: false,
pagination_types: [:first, :last, :page],
repo: MyApp.Repo,
query_opts: [prefix: "some-prefix"]
The look up order is:
- option passed to function
- option set for schema using
Flop.Schema
(only:max_limit
,:default_limit
,:default_order
and:pagination_types
) - option set in config module, if one is used (except
:for
,:default_order
and:count_query
; see section "Config modules" in the module documentation) - option set in global config (except
:for
,:default_order
and:count_query
) - default value (only
:cursor_value_func
)
@type order_direction() ::
:asc
| :asc_nulls_first
| :asc_nulls_last
| :desc
| :desc_nulls_first
| :desc_nulls_last
Represents the supported order direction values.
@type pagination_type() :: :offset | :page | :first | :last
Represents the pagination type.
:offset
- pagination using theoffset
andlimit
parameters:page
- pagination using thepage
andpage_size
parameters:first
- cursor-based pagination using thefirst
andafter
parameters:last
- cursor-based pagination using thelast
andbefore
parameters
@type t() :: %Flop{ after: String.t() | nil, before: String.t() | nil, filters: [Flop.Filter.t()] | nil, first: pos_integer() | nil, last: pos_integer() | nil, limit: pos_integer() | nil, offset: non_neg_integer() | nil, order_by: [atom() | String.t()] | nil, order_directions: [order_direction()] | nil, page: pos_integer() | nil, page_size: pos_integer() | nil }
Represents the query parameters for filtering, ordering and pagination.
fields
Fields
after
: Used for cursor-based pagination. Must be used withfirst
or a default limit.before
: Used for cursor-based pagination. Must be used withlast
or a default limit.limit
,offset
: Used for offset-based pagination.first
Used for cursor-based pagination. Can be used alone to begin pagination or withafter
last
Used for cursor-based pagination.page
,page_size
: Used for offset-based pagination as an alternative tooffset
andlimit
.order_by
: List of fields to order by. Fields can be restricted by derivingFlop.Schema
in your Ecto schema.order_directions
: List of order directions applied to the fields defined inorder_by
. If empty or the list is shorter than theorder_by
list,:asc
will be used as a default for each missing order direction.filters
: List of filters, seeFlop.Filter.t/0
.
Link to this section Functions
@spec all(Ecto.Queryable.t(), t(), [option()]) :: [any()]
Applies the given Flop to the given queryable and returns all matchings entries.
iex> Flop.all(Flop.Pet, %Flop{}, repo: Flop.Repo)
[]
You can also configure a default repo in your config files:
config :flop, repo: MyApp.Repo
This allows you to omit the third argument:
iex> Flop.all(Flop.Pet, %Flop{})
[]
Note that when using cursor-based pagination, the applied limit will be
first + 1
or last + 1
. The extra record is removed by Flop.run/3
, but
not by this function.
This function does not validate the given Flop struct. Be sure to validate
any user-generated parameters with validate/2
or validate!/2
before
passing them to this function.
Returns the names of the bindings that are required for the filters and order clauses of the given Flop.
The second argument is the schema module that derives Flop.Schema
.
For example, your schema module might define a join field called :owner_age
.
@derive {
Flop.Schema,
filterable: [:name, :owner_age],
sortable: [:name, :owner_age],
join_fields: [owner_age: {:owner, :age}]
}
If you pass a Flop with a filter on the :owner_age
field, the returned list
will include the :owner
binding.
iex> bindings(
...> %Flop{
...> filters: [%Flop.Filter{field: :owner_age, op: :==, value: 5}]
...> },
...> Flop.Pet
...> )
[:owner]
If on the other hand only normal fields or compound fields are used in the filter and order options, an empty list will be returned.
iex> bindings(
...> %Flop{
...> filters: [%Flop.Filter{field: :name, op: :==, value: "George"}]
...> },
...> Flop.Pet
...> )
[]
You can use this to dynamically build the join clauses needed for the query.
def list_pets(params) do
with {:ok, flop} <- Flop.validate(params, for: Pet) do
bindings = Flop.bindings(flop, Pet)
Pet
|> join_pet_assocs(bindings)
|> Flop.run(flop, for: Pet)
end
end
defp join_pet_assocs(q, bindings) when is_list(bindings) do
Enum.reduce(bindings, q, fn
:owner, acc ->
join(acc, :left, [p], o in assoc(p, :owner), as: :owner)
:toys, acc ->
join(acc, :left, [p], t in assoc(p, :toys), as: :toys)
end)
end
For more information about join fields, refer to the module documentation of
Flop.Schema
.
options
Options
:order
- Iffalse
, only bindings needed for filtering are included. Defaults totrue
.
@spec count(Ecto.Queryable.t(), t(), [option()]) :: non_neg_integer()
Returns the total count of entries matching the filter conditions of the Flop.
The pagination and ordering option are disregarded.
iex> Flop.count(Flop.Pet, %Flop{}, repo: Flop.Repo)
0
You can also configure a default repo in your config files:
config :flop, repo: MyApp.Repo
This allows you to omit the third argument:
iex> Flop.count(Flop.Pet, %Flop{})
0
You can override the default query by passing the :count_query
option. This
doesn't make a lot of sense when you use count/3
directly, but allows you to
optimize the count query when you use one of the run/3
,
validate_and_run/3
and validate_and_run!/3
functions.
query = join(Pet, :left, [p], o in assoc(p, :owner))
count_query = Pet
count(query, %Flop{}, count_query: count_query)
The filter parameters of the given Flop are applied to the custom count query.
This function does not validate the given Flop struct. Be sure to validate
any user-generated parameters with validate/2
or validate!/2
before
passing them to this function.
@spec current_order(t(), atom()) :: order_direction() | nil
Returns the current order direction for the given field.
examples
Examples
iex> flop = %Flop{order_by: [:name, :age], order_directions: [:desc]}
iex> current_order(flop, :name)
:desc
iex> current_order(flop, :age)
:asc
iex> current_order(flop, :species)
nil
@spec filter(Ecto.Queryable.t(), t(), [option()]) :: Ecto.Queryable.t()
Applies the filter
parameter of a Flop.t/0
to an Ecto.Queryable.t/0
.
Used by Flop.query/2
.
This function does not validate the given Flop struct. Be sure to validate
any user-generated parameters with validate/2
or validate!/2
before
passing them to this function.
Returns the option with the given key.
The look-up order is:
- the keyword list passed as the second argument
- the schema module that derives
Flop.Schema
, if the passed list includes the:for
option - the application environment
- the default passed as the last argument
Converts a map of filter conditions into a list of Flop filter params.
The default operator is :==
. nil
values are excluded from the result.
iex> map_to_filter_params(%{name: "George", age: 8, species: nil})
[
%{field: :age, op: :==, value: 8},
%{field: :name, op: :==, value: "George"}
]
iex> map_to_filter_params(%{"name" => "George", "age" => 8, "cat" => true})
[
%{"field" => "age", "op" => :==, "value" => 8},
%{"field" => "cat", "op" => :==, "value" => true},
%{"field" => "name", "op" => :==, "value" => "George"}
]
You can optionally pass a mapping from field names to operators as a map with atom keys.
iex> map_to_filter_params(
...> %{name: "George", age: 8, species: nil},
...> operators: %{name: :ilike_and}
...> )
[
%{field: :age, op: :==, value: 8},
%{field: :name, op: :ilike_and, value: "George"}
]
iex> map_to_filter_params(
...> %{"name" => "George", "age" => 8, "cat" => true},
...> operators: %{name: :ilike_and, age: :<=}
...> )
[
%{"field" => "age", "op" => :<=, "value" => 8},
%{"field" => "cat", "op" => :==, "value" => true},
%{"field" => "name", "op" => :ilike_and, "value" => "George"}
]
@spec meta(Ecto.Queryable.t() | [any()], t(), [option()]) :: Flop.Meta.t()
Returns meta information for the given query and flop that can be used for building the pagination links.
iex> Flop.meta(Flop.Pet, %Flop{limit: 10}, repo: Flop.Repo)
%Flop.Meta{
current_offset: 0,
current_page: 1,
end_cursor: nil,
flop: %Flop{limit: 10},
has_next_page?: false,
has_previous_page?: false,
next_offset: nil,
next_page: nil,
page_size: 10,
previous_offset: nil,
previous_page: nil,
start_cursor: nil,
total_count: 0,
total_pages: 0
}
The function returns both the current offset and the current page, regardless
of the pagination type. If the offset lies in between pages, the current page
number is rounded up. This means that it is possible that the values for
current_page
and next_page
can be identical. This can only occur if you
use offset/limit based pagination with arbitrary offsets, but in that case,
you will use the previous_offset
, current_offset
and next_offset
values
to render the pagination links anyway, so this shouldn't be a problem.
Unless cursor-based pagination is used, this function will run a query to figure get the total count of matching records.
This function does not validate the given Flop struct. Be sure to validate
any user-generated parameters with validate/2
or validate!/2
before
passing them to this function.
Converts key/value filter parameters at the root of a map, converts them into
a list of filter parameter maps and nests them under the :filters
key.
The second argument is a list of fields as atoms.
The opts
argument is passed to map_to_filter_params/2
.
examples
Examples
iex> nest_filters(%{name: "Peter", page_size: 10}, [:name])
%{filters: [%{field: :name, op: :==, value: "Peter"}], page_size: 10}
iex> nest_filters(%{"name" => "Peter"}, [:name])
%{"filters" => [%{"field" => "name", "op" => :==, "value" => "Peter"}]}
iex> nest_filters(%{name: "Peter"}, [:name], operators: %{name: :!=})
%{filters: [%{field: :name, op: :!=, value: "Peter"}]}
@spec order_by(Ecto.Queryable.t(), t(), [option()]) :: Ecto.Queryable.t()
Applies the order_by
and order_directions
parameters of a Flop.t/0
to an Ecto.Queryable.t/0
.
Used by Flop.query/2
.
This function does not validate the given Flop struct. Be sure to validate
any user-generated parameters with validate/2
or validate!/2
before
passing them to this function.
@spec paginate(Ecto.Queryable.t(), t(), [option()]) :: Ecto.Queryable.t()
Applies the pagination parameters of a Flop.t/0
to an
Ecto.Queryable.t/0
.
The function supports both offset
/limit
based pagination and
page
/page_size
based pagination.
If you validated the Flop.t/0
with Flop.validate/1
before, you can be
sure that the given Flop.t/0
only has pagination parameters set for one
pagination method. If you pass an unvalidated Flop.t/0
that has
pagination parameters set for multiple pagination methods, this function
will arbitrarily only apply one of the pagination methods.
Used by Flop.query/2
.
This function does not validate the given Flop struct. Be sure to validate
any user-generated parameters with validate/2
or validate!/2
before
passing them to this function.
Updates the order_by
and order_directions
values of a Flop
struct.
- If the field is not in the current
order_by
value, it will be prepended to the list. The order direction for the field will be set to:asc
. - If the field is already at the front of the
order_by
list, the order direction will be reversed. - If the field is already in the list, but not at the front, it will be moved
to the front and the order direction will be set to
:asc
.
example
Example
iex> flop = push_order(%Flop{}, :name)
iex> flop.order_by
[:name]
iex> flop.order_directions
[:asc]
iex> flop = push_order(flop, :age)
iex> flop.order_by
[:age, :name]
iex> flop.order_directions
[:asc, :asc]
iex> flop = push_order(flop, :age)
iex> flop.order_by
[:age, :name]
iex> flop.order_directions
[:desc, :asc]
iex> flop = push_order(flop, :species)
iex> flop.order_by
[:species, :age, :name]
iex> flop.order_directions
[:asc, :desc, :asc]
iex> flop = push_order(flop, :age)
iex> flop.order_by
[:age, :species, :name]
iex> flop.order_directions
[:asc, :asc, :asc]
If a string is passed as the second argument, it will be converted to an atom
using String.to_existing_atom/1
. If the atom does not exist, the Flop
struct will be returned unchanged.
iex> flop = push_order(%Flop{}, "name")
iex> flop.order_by
[:name]
iex> flop = push_order(%Flop{}, "this_atom_does_not_exist")
iex> flop.order_by
nil
Since the pagination cursor depends on the sort order, the :before
and
:after
parameters are reset.
iex> push_order(%Flop{order_by: [:id], after: "ABC"}, :name)
%Flop{order_by: [:name, :id], order_directions: [:asc], after: nil}
iex> push_order(%Flop{order_by: [:id], before: "DEF"}, :name)
%Flop{order_by: [:name, :id], order_directions: [:asc], before: nil}
@spec query(Ecto.Queryable.t(), t(), [option()]) :: Ecto.Queryable.t()
Adds clauses for filtering, ordering and pagination to a
Ecto.Queryable.t/0
.
The parameters are represented by the Flop.t/0
type. Any nil
values
will be ignored.
This function does not validate the given Flop struct. Be sure to validate
any user-generated parameters with validate/2
or validate!/2
before
passing them to this function.
examples
Examples
iex> flop = %Flop{limit: 10, offset: 19}
iex> Flop.query(Flop.Pet, flop)
#Ecto.Query<from p0 in Flop.Pet, limit: ^10, offset: ^19>
Or enhance an already defined query:
iex> require Ecto.Query
iex> flop = %Flop{limit: 10}
iex> Flop.Pet |> Ecto.Query.where(species: "dog") |> Flop.query(flop)
#Ecto.Query<from p0 in Flop.Pet, where: p0.species == "dog", limit: ^10>
Note that when using cursor-based pagination, the applied limit will be
first + 1
or last + 1
. The extra record is removed by Flop.run/3
.
Removes the after
and before
cursors from a Flop struct.
example
Example
iex> reset_cursors(%Flop{after: "A"})
%Flop{}
iex> reset_cursors(%Flop{before: "A"})
%Flop{}
Removes all filters from a Flop struct.
example
Example
iex> reset_filters(%Flop{filters: [
...> %Flop.Filter{field: :name, value: "Jim"}
...> ]})
%Flop{filters: []}
Removes the order parameters from a Flop struct.
example
Example
iex> reset_order(%Flop{order_by: [:name], order_directions: [:asc]})
%Flop{order_by: nil, order_directions: nil}
@spec run(Ecto.Queryable.t(), t(), [option()]) :: {[any()], Flop.Meta.t()}
Applies the given Flop to the given queryable, retrieves the data and the meta data.
This function does not validate the given flop parameters. You can validate
the parameters with Flop.validate/2
or Flop.validate!/2
, or you can use
Flop.validate_and_run/3
or Flop.validate_and_run!/3
instead of this
function.
iex> {data, meta} = Flop.run(Flop.Pet, %Flop{})
iex> data == []
true
iex> match?(%Flop.Meta{}, meta)
true
See the documentation for Flop.validate_and_run/3
for supported options.
@spec set_cursor(Flop.Meta.t(), :previous | :next) :: t()
Takes a Flop.Meta
struct and returns a Flop
struct with updated cursor
pagination params for going to either the previous or the next page.
See to_previous_cursor/1
and to_next_cursor/1
for details.
examples
Examples
iex> set_cursor(
...> %Flop.Meta{
...> flop: %Flop{first: 5, after: "a"},
...> has_previous_page?: true, start_cursor: "b"
...> },
...> :previous
...> )
%Flop{last: 5, before: "b"}
iex> set_cursor(
...> %Flop.Meta{
...> flop: %Flop{first: 5, after: "a"},
...> has_next_page?: true, end_cursor: "b"
...> },
...> :next
...> )
%Flop{first: 5, after: "b"}
@spec set_offset(t(), non_neg_integer() | binary()) :: t()
Sets the offset value of a Flop
struct while also removing/converting
pagination parameters for other pagination types.
iex> set_offset(%Flop{limit: 10, offset: 10}, 20)
%Flop{offset: 20, limit: 10}
iex> set_offset(%Flop{page: 5, page_size: 10}, 20)
%Flop{limit: 10, offset: 20, page: nil, page_size: nil}
iex> set_offset(%Flop{limit: 10, offset: 10}, "20")
%Flop{offset: 20, limit: 10}
The offset will not be allowed to go below 0.
iex> set_offset(%Flop{}, -5)
%Flop{offset: 0}
@spec set_page(t(), pos_integer() | binary()) :: t()
Sets the page value of a Flop
struct while also removing/converting
pagination parameters for other pagination types.
iex> set_page(%Flop{page: 2, page_size: 10}, 6)
%Flop{page: 6, page_size: 10}
iex> set_page(%Flop{limit: 10, offset: 20}, 8)
%Flop{limit: nil, offset: nil, page: 8, page_size: 10}
iex> set_page(%Flop{page: 2, page_size: 10}, "6")
%Flop{page: 6, page_size: 10}
The page number will not be allowed to go below 1.
iex> set_page(%Flop{}, -5)
%Flop{page: 1}
@spec to_next_cursor(Flop.Meta.t()) :: t()
Takes a Flop.Meta
struct and returns a Flop
struct with updated cursor
pagination params for going to the next page.
If there is no next page, the Flop
struct is return unchanged.
examples
Examples
iex> to_next_cursor(
...> %Flop.Meta{
...> flop: %Flop{first: 5, after: "a"},
...> has_next_page?: true, end_cursor: "b"
...> }
...> )
%Flop{first: 5, after: "b"}
iex> to_next_cursor(
...> %Flop.Meta{
...> flop: %Flop{last: 5, before: "b"},
...> has_next_page?: true, end_cursor: "a"
...> }
...> )
%Flop{first: 5, after: "a"}
iex> to_next_cursor(
...> %Flop.Meta{
...> flop: %Flop{first: 5, after: "a"},
...> has_next_page?: false, start_cursor: "b"
...> }
...> )
%Flop{first: 5, after: "a"}
@spec to_next_offset(t(), non_neg_integer() | nil) :: t()
Sets the offset of a Flop struct to the next page depending on the limit.
If the total count is given as the second argument, the offset will not be
increased if the last page has already been reached. You can get the total
count from the Flop.Meta
struct. If the Flop has an offset beyond the total
count, the offset will be set to the last page.
examples
Examples
iex> to_next_offset(%Flop{offset: 10, limit: 5})
%Flop{offset: 15, limit: 5}
iex> to_next_offset(%Flop{offset: 15, limit: 5}, 21)
%Flop{offset: 20, limit: 5}
iex> to_next_offset(%Flop{offset: 15, limit: 5}, 20)
%Flop{offset: 15, limit: 5}
iex> to_next_offset(%Flop{offset: 28, limit: 5}, 22)
%Flop{offset: 20, limit: 5}
iex> to_next_offset(%Flop{offset: -5, limit: 20})
%Flop{offset: 0, limit: 20}
@spec to_next_page(t(), non_neg_integer() | nil) :: t()
Sets the page of a Flop struct to the next page.
If the total number of pages is given as the second argument, the page number
will not be increased if the last page has already been reached. You can get
the total number of pages from the Flop.Meta
struct.
examples
Examples
iex> to_next_page(%Flop{page: 5})
%Flop{page: 6}
iex> to_next_page(%Flop{page: 5}, 6)
%Flop{page: 6}
iex> to_next_page(%Flop{page: 6}, 6)
%Flop{page: 6}
iex> to_next_page(%Flop{page: 7}, 6)
%Flop{page: 6}
iex> to_next_page(%Flop{page: -5})
%Flop{page: 1}
@spec to_previous_cursor(Flop.Meta.t()) :: t()
Takes a Flop.Meta
struct and returns a Flop
struct with updated cursor
pagination params for going to the previous page.
If there is no previous page, the Flop
struct is return unchanged.
examples
Examples
iex> to_previous_cursor(
...> %Flop.Meta{
...> flop: %Flop{first: 5, after: "a"},
...> has_previous_page?: true, start_cursor: "b"
...> }
...> )
%Flop{last: 5, before: "b"}
iex> to_previous_cursor(
...> %Flop.Meta{
...> flop: %Flop{last: 5, before: "b"},
...> has_previous_page?: true, start_cursor: "a"
...> }
...> )
%Flop{last: 5, before: "a"}
iex> to_previous_cursor(
...> %Flop.Meta{
...> flop: %Flop{first: 5, after: "b"},
...> has_previous_page?: false, start_cursor: "a"
...> }
...> )
%Flop{first: 5, after: "b"}
Sets the offset of a Flop struct to the page depending on the limit.
examples
Examples
iex> to_previous_offset(%Flop{offset: 20, limit: 10})
%Flop{offset: 10, limit: 10}
iex> to_previous_offset(%Flop{offset: 5, limit: 10})
%Flop{offset: 0, limit: 10}
iex> to_previous_offset(%Flop{offset: 0, limit: 10})
%Flop{offset: 0, limit: 10}
iex> to_previous_offset(%Flop{offset: -2, limit: 10})
%Flop{offset: 0, limit: 10}
Sets the page of a Flop struct to the previous page, but not less than 1.
examples
Examples
iex> to_previous_page(%Flop{page: 5})
%Flop{page: 4}
iex> to_previous_page(%Flop{page: 1})
%Flop{page: 1}
iex> to_previous_page(%Flop{page: -2})
%Flop{page: 1}
Same as Flop.validate/2
, but raises an Ecto.InvalidChangesetError
if the
parameters are invalid.
@spec validate(t() | map(), [option()]) :: {:ok, t()} | {:error, Flop.Meta.t()}
Validates a Flop.t/0
.
examples
Examples
iex> params = %{"limit" => 10, "offset" => 0, "texture" => "fluffy"}
iex> Flop.validate(params)
{:ok,
%Flop{
filters: [],
limit: 10,
offset: 0,
order_by: nil,
order_directions: nil,
page: nil,
page_size: nil
}}
iex> flop = %Flop{offset: -1}
iex> {:error, %Flop.Meta{} = meta} = Flop.validate(flop)
iex> meta.errors
[
offset: [
{"must be greater than or equal to %{number}",
[validation: :number, kind: :greater_than_or_equal_to, number: 0]}
]
]
It also makes sure that only one pagination method is used.
iex> params = %{limit: 10, offset: 0, page: 5, page_size: 10}
iex> {:error, %Flop.Meta{} = meta} = Flop.validate(params)
iex> meta.errors
[limit: [{"cannot combine multiple pagination types", []}]]
If you derived Flop.Schema
in your Ecto schema to define the filterable
and sortable fields, you can pass the module name to the function to validate
that only allowed fields are used. The function will also apply any default
values set for the schema.
iex> params = %{"order_by" => ["species"]}
iex> {:error, %Flop.Meta{} = meta} = Flop.validate(params, for: Flop.Pet)
iex> [order_by: [{msg, [_, {_, enum}]}]] = meta.errors
iex> msg
"has an invalid entry"
iex> enum
[:name, :age, :owner_name, :owner_age]
Note that currently, trying to use an existing field that is not allowed as
seen above will result in the error message has an invalid entry
, while
trying to use a field name that does not exist in the schema (or more
precisely: a field name that doesn't exist as an atom) will result in
the error message is invalid
. This might change in the future.
@spec validate_and_run!(Ecto.Queryable.t(), map() | t(), [option()]) :: {[any()], Flop.Meta.t()}
Same as Flop.validate_and_run/3
, but raises on error.
@spec validate_and_run(Ecto.Queryable.t(), map() | t(), [option()]) :: {:ok, {[any()], Flop.Meta.t()}} | {:error, Flop.Meta.t()}
Validates the given flop parameters and retrieves the data and meta data on success.
iex> {:ok, {[], %Flop.Meta{}}} =
...> Flop.validate_and_run(Flop.Pet, %Flop{}, for: Flop.Pet)
iex> {:error, %Flop.Meta{} = meta} =
...> Flop.validate_and_run(Flop.Pet, %Flop{limit: -1})
iex> meta.errors
[
limit: [
{"must be greater than %{number}",
[validation: :number, kind: :greater_than, number: 0]}
]
]
options
Options
for
: Passed toFlop.validate/2
.repo
: TheEcto.Repo
module. Required if no default repo is configured.cursor_value_func
: An arity-2 function to be used to retrieve an unencoded cursor value from a query result item and theorder_by
fields. Defaults toFlop.Cursor.get_cursor_from_node/2
.count_query
: Lets you override the base query for counting, e.g. if you don't want to include unnecessary joins. The filter parameters are applied to the given query. See alsoFlop.count/3
.