DDD.Filter (vicander v0.1.1)

Copy Markdown View Source

A small, safe filter language that becomes a SQL WHERE, for the listing functions of DDD.Repository.Ecto adapters and DDD.Projections (list/1, page/1, slice/1, count/1).

[owner_id: id, status: :open]                       # all of them (AND)
[open_count: {:gt, 3}]                              # a comparison
{:or, [[status: :open], [title: {:like, "Buy%"}]]}
{:not, [status: :done]}
[{:spec, MyApp.Specs.Overdue}]                      # a specification, see below

Forms (field is an atom, value is in its domain form: atoms, Dates, ids as strings):

formmeaning
{field, value}equal (nil means IS NULL)
{field, {op, value}} or {field, op, value}op is :eq, :neq, :gt, :gte, :lt, :lte, :in, :not_in, :like, :ilike
[filter, ...] or a keyword list or a mapevery one of them
{:and, [filter]}, {:or, [filter]}, {:not, filter}combinators (so :and, :or, :not can't be field names here)
{:spec, spec}a DDD.Specification translated by DDD.Specification.to_filter/2

Values are never interpolated into SQL: they are bound parameters, converted with DDD.Type.dump/2 the same way find_by_* does. An unknown field, an unknown operator or a malformed expression is a business error with code :invalid_filter (so a filter built from a client's request can be answered with it), never a crash or an injection.

Summary

Functions

Build the Ecto.Query.dynamic/2 for filter. types maps each field to its domain type (%{title: :string, ...}); only those fields can be filtered on.

Functions

build(filter, types)

@spec build(term(), %{required(atom()) => term()}) ::
  {:ok, Ecto.Query.dynamic_expr()} | {:error, DDD.Error.t()}

Build the Ecto.Query.dynamic/2 for filter. types maps each field to its domain type (%{title: :string, ...}); only those fields can be filtered on.