Skip to content

Commit

Permalink
feat(Ash v3)!: follow ash v3 upgrade guide
Browse files Browse the repository at this point in the history
  • Loading branch information
mbaertschi committed Aug 7, 2024
1 parent cb0da9e commit 593c063
Show file tree
Hide file tree
Showing 18 changed files with 223 additions and 201 deletions.
38 changes: 21 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<!-- MDOC -->

AshPagify is an Elixir library designed to easily add full-text search, scoping, filtering,
ordering, and pagination APIs for the [Ash Framework](https://hexdocs.pm/ash)
ordering, and pagination APIs for the [Ash Framework](https://hexdocs.pm/ash).

It takes concepts from `Flop`, `Flop.Phoenix`, `Ash` and `AshPhoenix.FilterForm` and
combines them into a single library.
Expand Down Expand Up @@ -179,15 +179,17 @@ defmodule YourApp.Resource.Post
end

calculations do
# provide the default tsvector calculation for full-text search
# provide your default `tsvector` calculation for full-text search
calculate :tsvector,
AshPostgres.Tsvector,
expr(
fragment("to_tsvector('simple', coalesce(?, '')) || to_tsvector('simple', coalesce(?, ''))",
name,
title
)
)
AshPostgres.Tsvector,
expr(
fragment(
"to_tsvector('simple', coalesce(?, '')) || to_tsvector('simple', coalesce(?, ''))",
name,
title
)
),
public?: true
end
#...
end
Expand Down Expand Up @@ -274,21 +276,23 @@ you need to either `use AshPagify.Tsearch` in your module or implement the `full
```elixir
# provide the default tsvector calculation for full-text search
calculate :tsvector,
AshPostgres.Tsvector,
expr(
fragment("to_tsvector('simple', coalesce(?, '')) || to_tsvector('simple', coalesce(?, ''))",
name,
title
)
)
AshPostgres.Tsvector,
expr(
fragment(
"to_tsvector('simple', coalesce(?, '')) || to_tsvector('simple', coalesce(?, ''))",
name,
title
)
),
public?: true
```

Or if you want to use a generated tsvector column, you can replace the fields
part with the name of your generated tsvector column:

```elixir
# use a tsvector column from the database
calculate :tsvector, AshPostgres.Tsvector, expr(fragment("?", tsv))
calculate :tsvector, AshPostgres.Tsvector, expr(tsv), public?: true
```

You can also configure `dynamic` tsvectors based on user input. Have a look at the
Expand Down
2 changes: 1 addition & 1 deletion config/config.exs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import Config

config :ash_pagify,
ash_apis: [],
ash_domains: [],
env: Mix.env()

config :ash_uuid, :otp_app, :ash_pagify
Expand Down
4 changes: 4 additions & 0 deletions config/test.exs
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import Config

config :ash_pagify,
ash_domains: [AshPagify.Factory.Domain],
env: Mix.env()

config :logger, level: :warning

# config :ash, disable_async?: true
Expand Down
17 changes: 10 additions & 7 deletions lib/ash_pagify.ex
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ defmodule AshPagify do
alias AshPagify.Misc
alias AshPagify.Validation

require Ash.Expr
require Ash.Query
require Logger

Expand Down Expand Up @@ -718,18 +719,20 @@ defmodule AshPagify do
def search(%Ash.Query{} = q, %AshPagify{search: ""}, _opts), do: q

def search(%Ash.Query{} = q, %AshPagify{search: search} = ash_pagify, opts) do
tsquery = AshPagify.Tsearch.tsquery(search, opts)
tsquery = Ash.Query.expr(tsquery(search: tsquery))
tsvector = AshPagify.Tsearch.tsvector(opts)
tsquery_str = AshPagify.Tsearch.tsquery(search, opts)
tsquery_expr = Ash.Expr.expr(tsquery(search: ^tsquery_str))
tsvector_expr = AshPagify.Tsearch.tsvector(opts)

q
|> Ash.Query.filter(full_text_search(tsvector: tsvector, tsquery: tsquery))
|> maybe_put_ts_rank(ash_pagify, tsvector, tsquery)
|> Ash.Query.filter(full_text_search(tsvector: ^tsvector_expr, tsquery: ^tsquery_expr))
|> maybe_put_ts_rank(ash_pagify, tsvector_expr, tsquery_expr)
end

defp maybe_put_ts_rank(%Ash.Query{} = q, %AshPagify{order_by: order_by}, tsvector, tsquery)
defp maybe_put_ts_rank(%Ash.Query{} = q, %AshPagify{order_by: order_by}, tsvector_expr, tsquery_expr)
when is_nil(order_by) or order_by == [] do
Ash.Query.sort(q, full_text_search_rank: {:desc, %{tsvector: tsvector, tsquery: tsquery}})
Ash.Query.sort(q,
full_text_search_rank: {%{tsvector: tsvector_expr, tsquery: tsquery_expr}, :desc}
)
end

defp maybe_put_ts_rank(%Ash.Query{} = q, _, _, _), do: q
Expand Down
20 changes: 10 additions & 10 deletions lib/ash_pagify/filter_form.ex
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ defmodule AshPagify.FilterForm do
filter_form = AshPagify.FilterForm.new(MyApp.Payroll.Employee)
```
FilterForm's comprise 2 concepts, predicates and groups. Predicates are the simple boolean
FilterForm's comprise two concepts, predicates and groups. Predicates are the simple boolean
expressions you can use to build a query (`name == "Joe"`), and groups can be used to group
predicates and more groups together. Groups can apply `and` or `or` operators to its nested
components.
Expand All @@ -37,7 +37,7 @@ defmodule AshPagify.FilterForm do
```elixir
filter_form = AshPagify.FilterForm.validate(socket.assigns.filter_form, params)
# Generate a query and pass it to the Api
# Generate a query and pass it to the Domain
query = AshPagify.FilterForm.filter!(MyApp.Payroll.Employee, filter_form)
filtered_employees = MyApp.Payroll.read!(query)
Expand Down Expand Up @@ -208,7 +208,7 @@ defmodule AshPagify.FilterForm do
alias AshPhoenix.FilterForm.Arguments
alias AshPhoenix.FilterForm.Predicate

require Ash.Query
require Ash.Expr

defstruct [
:id,
Expand Down Expand Up @@ -307,10 +307,10 @@ defmodule AshPagify.FilterForm do
Create a new filter form.
Options:
#{Spark.OptionsHelpers.docs(@new_opts)}
#{Spark.Options.docs(@new_opts)}
"""
def new(resource, opts \\ []) do
opts = Spark.OptionsHelpers.validate!(opts, @new_opts)
opts = Spark.Options.validate!(opts, @new_opts)
params = opts[:params]

params =
Expand Down Expand Up @@ -609,7 +609,7 @@ defmodule AshPagify.FilterForm do
defp resource_ref(resource, path, field, arguments) do
case Resource.Info.public_calculation(Resource.Info.related(resource, path), field) do
nil ->
{:ok, Query.expr(ref(^field, ^path))}
{:ok, Ash.Expr.expr(^Ash.Expr.ref(List.wrap(path), field))}

calc ->
case Query.validate_calculation_arguments(
Expand Down Expand Up @@ -1095,10 +1095,10 @@ defmodule AshPagify.FilterForm do
Options:
#{Spark.OptionsHelpers.docs(@add_predicate_opts)}
#{Spark.Options.docs(@add_predicate_opts)}
"""
def add_predicate(form, field, operator_or_function, value, opts \\ []) do
opts = Spark.OptionsHelpers.validate!(opts, @add_predicate_opts)
opts = Spark.Options.validate!(opts, @add_predicate_opts)

predicate_id = Ash.UUID.generate()

Expand Down Expand Up @@ -1297,10 +1297,10 @@ defmodule AshPagify.FilterForm do
Options:
#{Spark.OptionsHelpers.docs(@add_group_opts)}
#{Spark.Options.docs(@add_group_opts)}
"""
def add_group(form, opts \\ []) do
opts = Spark.OptionsHelpers.validate!(opts, @add_group_opts)
opts = Spark.Options.validate!(opts, @add_group_opts)
group_id = Ash.UUID.generate()

group = %__MODULE__{
Expand Down
98 changes: 54 additions & 44 deletions lib/ash_pagify/tsearch.ex
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,19 @@ defmodule AshPagify.Tsearch do
```elixir
defmodule MyApp.Resource do
use AshPagify.Tsearch
require Ash.Query
require Ash.Expr
calculations do
calculate :tsvector,
AshPostgres.Tsvector,
expr(
fragment("to_tsvector('simple', coalesce(?, '')) || to_tsvector('simple', coalesce(?, ''))",
name,
title
)
)
AshPostgres.Tsvector,
expr(
fragment(
"to_tsvector('simple', coalesce(?, '')) || to_tsvector('simple', coalesce(?, ''))",
name,
title
)
),
public?: true
end
end
end
Expand All @@ -38,14 +40,15 @@ defmodule AshPagify.Tsearch do
```elixir
defmodule MyApp.Resource do
use AshPagify.Tsearch, only: [:full_text_search, :full_text_search_rank]
require Ash.Query
require Ash.Expr
calculations do
calculate :tsquery,
AshPostgres.Tsquery,
# use english dictionary unaccent PostgreSQL extension
expr(fragment("to_tsquery('english', unaccent(?))", ^arg(:search)))
)
AshPostgres.Tsquery,
# use english dictionary unaccent PostgreSQL extension
expr(fragment("to_tsquery('english', unaccent(?))", ^arg(:search))) do
public? true
argument :search, :string, allow_expr?: true, allow_nil?: false
end
...
Expand Down Expand Up @@ -135,47 +138,53 @@ defmodule AshPagify.Tsearch do
```elixir
defmodule MyApp.Resource do
use AshPagify.Tsearch
require Ash.Query
require Ash.Expr
def full_text_search do
[
tsvector_column: [
custom_tsvector: Ash.Query.expr(custom_tsvector),
another_custom_tsvector: Ash.Query.expr(another_custom_tsvector),
custom_tsvector: Ash.Expr.expr(custom_tsvector),
another_custom_tsvector: Ash.Expr.expr(another_custom_tsvector),
]
]
end
calculations do
# default tsvector calculation
calculate :tsvector,
AshPostgres.Tsvector,
expr(
fragment("to_tsvector('simple', coalesce(?, '')) || to_tsvector('simple', coalesce(?, ''))",
name,
title
)
)
AshPostgres.Tsvector,
expr(
fragment(
"to_tsvector('simple', coalesce(?, '')) || to_tsvector('simple', coalesce(?, ''))",
name,
title
)
),
public?: true
end
# custom tsvector calculation
calculate :custom_tsvector,
AshPostgres.Tsvector,
expr(
fragment("to_tsvector('simple', coalesce(?, ''))",
name
)
)
AshPostgres.Tsvector,
expr(
fragment(
"to_tsvector('simple', coalesce(?, ''))",
name
)
),
public?: true
end
# another custom tsvector calculation
calculate :another_custom_tsvector,
AshPostgres.Tsvector,
expr(
fragment("to_tsvector('simple', coalesce(?, ''))",
title
)
)
AshPostgres.Tsvector,
expr(
fragment(
"to_tsvector('simple', coalesce(?, ''))",
title
)
),
public?: true
end
end
end
Expand All @@ -194,7 +203,7 @@ defmodule AshPagify.Tsearch do

alias AshPagify.Misc

require Ash.Query
require Ash.Expr

@disallowed_tsquery_characters ~r/['?\\:‘’ʻʼ\|\&]/u

Expand Down Expand Up @@ -285,28 +294,28 @@ defmodule AshPagify.Tsearch do
coalesce_tsvector(tsvector, tsvector_column)
end

defp coalesce_tsvector(nil, nil), do: Ash.Query.expr(tsvector)
defp coalesce_tsvector(_, nil), do: Ash.Query.expr(tsvector)
defp coalesce_tsvector(nil, nil), do: Ash.Expr.expr(tsvector)
defp coalesce_tsvector(_, nil), do: Ash.Expr.expr(tsvector)

defp coalesce_tsvector(key, tsvector_column) when is_binary(key) and is_list(tsvector_column) do
coalesce_tsvector(String.to_existing_atom(key), tsvector_column)
rescue
ArgumentError -> Ash.Query.expr(tsvector)
ArgumentError -> Ash.Expr.expr(tsvector)
end

defp coalesce_tsvector(key, tsvector_column) when is_atom(key) and is_list(tsvector_column) do
Keyword.get(tsvector_column, key, Ash.Query.expr(tsvector))
Keyword.get(tsvector_column, key, Ash.Expr.expr(tsvector))
end

defp coalesce_tsvector(nil, tsvector_column) do
if is_tuple(tsvector_column) or is_list(tsvector_column) do
Ash.Query.expr(tsvector)
Ash.Expr.expr(tsvector)
else
tsvector_column
end
end

defp coalesce_tsvector(_, _), do: Ash.Query.expr(tsvector)
defp coalesce_tsvector(_, _), do: Ash.Expr.expr(tsvector)

@doc """
Returns the tsquery expression for the given search term and options.
Expand Down Expand Up @@ -403,8 +412,6 @@ defmodule AshPagify.Tsearch do
defp blank?(t), do: String.trim(t) == ""

defmacro __using__(opts \\ []) do
require Ash.Query

only = Keyword.get(opts, :only, [])

quote do
Expand All @@ -413,6 +420,7 @@ defmodule AshPagify.Tsearch do
calculate :full_text_search,
:boolean,
expr(fragment("(? @@ ?)", ^arg(:tsvector), ^arg(:tsquery))) do
public? true
argument :tsvector, AshPostgres.Tsvector, allow_expr?: true, allow_nil?: false
argument :tsquery, AshPostgres.Tsquery, allow_expr?: true, allow_nil?: false
end
Expand All @@ -424,6 +432,7 @@ defmodule AshPagify.Tsearch do
calculate :full_text_search_rank,
:float,
expr(fragment("ts_rank(?, ?)", ^arg(:tsvector), ^arg(:tsquery))) do
public? true
argument :tsvector, AshPostgres.Tsvector, allow_expr?: true, allow_nil?: false
argument :tsquery, AshPostgres.Tsquery, allow_expr?: true, allow_nil?: false
end
Expand All @@ -435,6 +444,7 @@ defmodule AshPagify.Tsearch do
calculate :tsquery,
AshPostgres.Tsquery,
expr(fragment("to_tsquery('simple', ?)", ^arg(:search))) do
public? true
argument :search, :string, allow_expr?: true, allow_nil?: false
end
end
Expand Down
Loading

0 comments on commit 593c063

Please sign in to comment.