From 042d4cdb43ee6ca26b9f548a8b86aa57e9386fbe Mon Sep 17 00:00:00 2001 From: Adi Date: Mon, 18 Dec 2017 08:48:59 -0500 Subject: [PATCH 01/64] Add expected CHANGELOG + version --- CHANGELOG.md | 17 +++++++++++++++++ mix.exs | 4 ++-- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5947f4e..5218ce4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,22 @@ # Versions CHANGELOG +## Version: 1.3.0 + +### TODO: + +- Bring back `__using__` +- Make default hooks more generalized +- Add NoPrimaryKey pagination hook or add a generalized version of current hook +- Change the way configurations are done +- Update Documentation + +### DONE: + + +## Version: 1.2.0 + +- Faster Pagination Hooks + ## Version: 1.1.0 ### Changes to Rummage as whole: diff --git a/mix.exs b/mix.exs index fa55e71..d98a5c3 100644 --- a/mix.exs +++ b/mix.exs @@ -1,14 +1,14 @@ defmodule Rummage.Ecto.Mixfile do use Mix.Project - @version "1.2.0" + @version "1.3.0-rc" @url "https://github.com/aditya7iyengar/rummage_ecto" def project do [ app: :rummage_ecto, version: @version, - elixir: "~> 1.3.4", + elixir: "~> 1.4.5", deps: deps(), build_embedded: Mix.env == :prod, start_permanent: Mix.env == :prod, From 0b1ca805085fb2c3ce79b16f8b3f6d31fb2fcdd2 Mon Sep 17 00:00:00 2001 From: Adi Date: Mon, 18 Dec 2017 10:27:01 -0500 Subject: [PATCH 02/64] Add elixir and otp version --- .tool-versions | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .tool-versions diff --git a/.tool-versions b/.tool-versions new file mode 100644 index 0000000..99928ca --- /dev/null +++ b/.tool-versions @@ -0,0 +1,2 @@ +elixir 1.4.5 +erlang 20.1 From 4c027e34ae881818344c332772c12387b206c972 Mon Sep 17 00:00:00 2001 From: Adi Date: Mon, 18 Dec 2017 10:28:03 -0500 Subject: [PATCH 03/64] Bump Ecto version to 2.2 --- mix.exs | 2 +- mix.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/mix.exs b/mix.exs index d98a5c3..33e3e79 100644 --- a/mix.exs +++ b/mix.exs @@ -51,7 +51,7 @@ defmodule Rummage.Ecto.Mixfile do defp deps do [ {:credo, "~> 0.5", only: [:dev, :test]}, - {:ecto, "~> 2.1"}, + {:ecto, "~> 2.2"}, {:excoveralls, "~> 0.3", only: :test}, {:ex_doc, "~> 0.14", only: :dev, runtime: false}, {:inch_ex, "~> 0.5", only: [:dev, :test, :docs]}, diff --git a/mix.lock b/mix.lock index 481a4bb..5d2493a 100644 --- a/mix.lock +++ b/mix.lock @@ -2,11 +2,11 @@ "certifi": {:hex, :certifi, "0.7.0", "861a57f3808f7eb0c2d1802afeaae0fa5de813b0df0979153cbafcd853ababaf", [:rebar3], [], "hexpm"}, "connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [:mix], [], "hexpm"}, "credo": {:hex, :credo, "0.6.1", "a941e2591bd2bd2055dc92b810c174650b40b8290459c89a835af9d59ac4a5f8", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}], "hexpm"}, - "db_connection": {:hex, :db_connection, "1.1.0", "b2b88db6d7d12f99997b584d09fad98e560b817a20dab6a526830e339f54cdb3", [:mix], [{:connection, "~> 1.0.2", [hex: :connection, repo: "hexpm", optional: false]}, {:poolboy, "~> 1.5", [hex: :poolboy, repo: "hexpm", optional: true]}, {:sbroker, "~> 1.0", [hex: :sbroker, repo: "hexpm", optional: true]}], "hexpm"}, - "decimal": {:hex, :decimal, "1.3.1", "157b3cedb2bfcb5359372a7766dd7a41091ad34578296e951f58a946fcab49c6", [:mix], [], "hexpm"}, + "db_connection": {:hex, :db_connection, "1.1.2", "2865c2a4bae0714e2213a0ce60a1b12d76a6efba0c51fbda59c9ab8d1accc7a8", [], [{:connection, "~> 1.0.2", [hex: :connection, repo: "hexpm", optional: false]}, {:poolboy, "~> 1.5", [hex: :poolboy, repo: "hexpm", optional: true]}, {:sbroker, "~> 1.0", [hex: :sbroker, repo: "hexpm", optional: true]}], "hexpm"}, + "decimal": {:hex, :decimal, "1.4.1", "ad9e501edf7322f122f7fc151cce7c2a0c9ada96f2b0155b8a09a795c2029770", [], [], "hexpm"}, "dialyxir": {:hex, :dialyxir, "0.5.0", "5bc543f9c28ecd51b99cc1a685a3c2a1a93216990347f259406a910cf048d1d7", [:mix], []}, "earmark": {:hex, :earmark, "1.1.1", "433136b7f2e99cde88b745b3a0cfc3fbc81fe58b918a09b40fce7f00db4d8187", [:mix], [], "hexpm"}, - "ecto": {:hex, :ecto, "2.1.3", "ffb24e150b519a4c0e4c84f9eabc8587199389bc499195d5d1a93cd3b2d9a045", [:mix], [{:db_connection, "~> 1.1", [hex: :db_connection, repo: "hexpm", optional: true]}, {:decimal, "~> 1.2", [hex: :decimal, repo: "hexpm", optional: false]}, {:mariaex, "~> 0.8.0", [hex: :mariaex, repo: "hexpm", optional: true]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: true]}, {:poolboy, "~> 1.5", [hex: :poolboy, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.13.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:sbroker, "~> 1.0", [hex: :sbroker, repo: "hexpm", optional: true]}], "hexpm"}, + "ecto": {:hex, :ecto, "2.2.7", "2074106ff4a5cd9cb2b54b12ca087c4b659ddb3f6b50be4562883c1d763fb031", [], [{:db_connection, "~> 1.1", [hex: :db_connection, repo: "hexpm", optional: true]}, {:decimal, "~> 1.2", [hex: :decimal, repo: "hexpm", optional: false]}, {:mariaex, "~> 0.8.0", [hex: :mariaex, repo: "hexpm", optional: true]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: true]}, {:poolboy, "~> 1.5", [hex: :poolboy, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.13.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:sbroker, "~> 1.0", [hex: :sbroker, repo: "hexpm", optional: true]}], "hexpm"}, "ex_doc": {:hex, :ex_doc, "0.14.5", "c0433c8117e948404d93ca69411dd575ec6be39b47802e81ca8d91017a0cf83c", [:mix], [{:earmark, "~> 1.0", [hex: :earmark, repo: "hexpm", optional: false]}], "hexpm"}, "excoveralls": {:hex, :excoveralls, "0.6.2", "0e993d096f1fbb6e70a3daced5c89aac066bda6bce57829622aa2d1e2b338cfb", [:mix], [{:exjsx, "~> 3.0", [hex: :exjsx, repo: "hexpm", optional: false]}, {:hackney, ">= 0.12.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"}, "exjsx": {:hex, :exjsx, "3.2.1", "1bc5bf1e4fd249104178f0885030bcd75a4526f4d2a1e976f4b428d347614f0f", [:mix], [{:jsx, "~> 2.8.0", [hex: :jsx, repo: "hexpm", optional: false]}], "hexpm"}, @@ -18,5 +18,5 @@ "mimerl": {:hex, :mimerl, "1.0.2", "993f9b0e084083405ed8252b99460c4f0563e41729ab42d9074fd5e52439be88", [:rebar3], [], "hexpm"}, "poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], [], "hexpm"}, "poolboy": {:hex, :poolboy, "1.5.1", "6b46163901cfd0a1b43d692657ed9d7e599853b3b21b95ae5ae0a777cf9b6ca8", [:rebar], [], "hexpm"}, - "postgrex": {:hex, :postgrex, "0.13.0", "e101ab47d0725955c5c8830ae8812412992e02e4bd9db09e17abb0a5d82d09c7", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 1.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm"}, + "postgrex": {:hex, :postgrex, "0.13.3", "c277cfb2a9c5034d445a722494c13359e361d344ef6f25d604c2353185682bfc", [], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 1.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm"}, "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.1", "28a4d65b7f59893bc2c7de786dec1e1555bd742d336043fe644ae956c3497fbe", [:make, :rebar], [], "hexpm"}} From 9deeb8b1d59ae459fffb80892247ca0b47c7c233 Mon Sep 17 00:00:00 2001 From: Adi Date: Mon, 18 Dec 2017 11:11:18 -0500 Subject: [PATCH 04/64] Add sqlilte3 files --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 002d321..3e751f0 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,6 @@ erl_crash.dump # Inch CI /docs + +*.sqlite3 +*.sqlite3-* From e5564970075a8ae79a2082b11eb90d1b487af348 Mon Sep 17 00:00:00 2001 From: Adi Date: Mon, 18 Dec 2017 11:11:32 -0500 Subject: [PATCH 05/64] Update elixir and otp version in travis yml --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 36b6c2e..e8acd95 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,8 @@ language: elixir elixir: - - 1.3.4 + - 1.4.5 otp_release: - - 19.0 + - 20.2 sudo: false env: - MIX_ENV=test ECTO_PGSQL_USER=postgres ECTO_PGSQL_PASSWORD= From f64ff36bdd76a9c91127493ea3f2f99fe070f458 Mon Sep 17 00:00:00 2001 From: Adi Date: Mon, 18 Dec 2017 11:13:15 -0500 Subject: [PATCH 06/64] Use sqlite3 adapter for testing: - It's lightweight - Requires minimal setup - Can be tested anywhere --- config/test.exs | 5 +---- mix.exs | 4 ++-- mix.lock | 4 ++++ test/support/repo.ex | 2 +- 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/config/test.exs b/config/test.exs index 7b60e01..aba6087 100644 --- a/config/test.exs +++ b/config/test.exs @@ -12,8 +12,5 @@ config :rummage_ecto, ecto_repos: [Rummage.Ecto.Repo] config :rummage_ecto, Rummage.Ecto.Repo, adapter: Ecto.Adapters.Postgres, - username: System.get_env("ECTO_PGSQL_USER"), - password: System.get_env("ECTO_PGSQL_PASSWORD"), - database: "rummage_ecto_test", - hostname: "localhost", + database: "rummage_ecto_test.sqlite3", pool: Ecto.Adapters.SQL.Sandbox diff --git a/mix.exs b/mix.exs index 33e3e79..b920167 100644 --- a/mix.exs +++ b/mix.exs @@ -34,7 +34,7 @@ defmodule Rummage.Ecto.Mixfile do applications: [ :logger, :ecto, - :postgrex, + :sqlite_ecto2, ], ] end @@ -55,7 +55,7 @@ defmodule Rummage.Ecto.Mixfile do {:excoveralls, "~> 0.3", only: :test}, {:ex_doc, "~> 0.14", only: :dev, runtime: false}, {:inch_ex, "~> 0.5", only: [:dev, :test, :docs]}, - {:postgrex, ">= 0.0.0", only: [:test]}, + {:sqlite_ecto2, "~> 2.2", only: :test}, ] end diff --git a/mix.lock b/mix.lock index 5d2493a..b4e0e9f 100644 --- a/mix.lock +++ b/mix.lock @@ -7,6 +7,7 @@ "dialyxir": {:hex, :dialyxir, "0.5.0", "5bc543f9c28ecd51b99cc1a685a3c2a1a93216990347f259406a910cf048d1d7", [:mix], []}, "earmark": {:hex, :earmark, "1.1.1", "433136b7f2e99cde88b745b3a0cfc3fbc81fe58b918a09b40fce7f00db4d8187", [:mix], [], "hexpm"}, "ecto": {:hex, :ecto, "2.2.7", "2074106ff4a5cd9cb2b54b12ca087c4b659ddb3f6b50be4562883c1d763fb031", [], [{:db_connection, "~> 1.1", [hex: :db_connection, repo: "hexpm", optional: true]}, {:decimal, "~> 1.2", [hex: :decimal, repo: "hexpm", optional: false]}, {:mariaex, "~> 0.8.0", [hex: :mariaex, repo: "hexpm", optional: true]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: true]}, {:poolboy, "~> 1.5", [hex: :poolboy, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.13.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:sbroker, "~> 1.0", [hex: :sbroker, repo: "hexpm", optional: true]}], "hexpm"}, + "esqlite": {:hex, :esqlite, "0.2.3", "1a8b60877fdd3d50a8a84b342db04032c0231cc27ecff4ddd0d934485d4c0cd5", [], [], "hexpm"}, "ex_doc": {:hex, :ex_doc, "0.14.5", "c0433c8117e948404d93ca69411dd575ec6be39b47802e81ca8d91017a0cf83c", [:mix], [{:earmark, "~> 1.0", [hex: :earmark, repo: "hexpm", optional: false]}], "hexpm"}, "excoveralls": {:hex, :excoveralls, "0.6.2", "0e993d096f1fbb6e70a3daced5c89aac066bda6bce57829622aa2d1e2b338cfb", [:mix], [{:exjsx, "~> 3.0", [hex: :exjsx, repo: "hexpm", optional: false]}, {:hackney, ">= 0.12.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"}, "exjsx": {:hex, :exjsx, "3.2.1", "1bc5bf1e4fd249104178f0885030bcd75a4526f4d2a1e976f4b428d347614f0f", [:mix], [{:jsx, "~> 2.8.0", [hex: :jsx, repo: "hexpm", optional: false]}], "hexpm"}, @@ -19,4 +20,7 @@ "poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], [], "hexpm"}, "poolboy": {:hex, :poolboy, "1.5.1", "6b46163901cfd0a1b43d692657ed9d7e599853b3b21b95ae5ae0a777cf9b6ca8", [:rebar], [], "hexpm"}, "postgrex": {:hex, :postgrex, "0.13.3", "c277cfb2a9c5034d445a722494c13359e361d344ef6f25d604c2353185682bfc", [], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 1.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm"}, + "sbroker": {:hex, :sbroker, "1.0.0", "28ff1b5e58887c5098539f236307b36fe1d3edaa2acff9d6a3d17c2dcafebbd0", [], [], "hexpm"}, + "sqlite_ecto2": {:hex, :sqlite_ecto2, "2.2.2", "7a3e5c0521e1cb6e30a4907ba4d952b97db9b2ab5d1a4806ceeb66a10b23ba65", [], [{:connection, "~> 1.0.3", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 1.1.0", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.2", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 2.2.2", [hex: :ecto, repo: "hexpm", optional: false]}, {:esqlite, "~> 0.2.3", [hex: :esqlite, repo: "hexpm", optional: false]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.13.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:sbroker, "~> 1.0", [hex: :sbroker, repo: "hexpm", optional: false]}, {:sqlitex, "~> 1.3.2 or ~> 1.4", [hex: :sqlitex, repo: "hexpm", optional: false]}], "hexpm"}, + "sqlitex": {:hex, :sqlitex, "1.3.3", "3aac5fd702be346f71d9de6e01702c9954484cd0971aa443490bb3bde045d919", [], [{:decimal, "~> 1.1", [hex: :decimal, repo: "hexpm", optional: false]}, {:esqlite, "~> 0.2.3", [hex: :esqlite, repo: "hexpm", optional: false]}], "hexpm"}, "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.1", "28a4d65b7f59893bc2c7de786dec1e1555bd742d336043fe644ae956c3497fbe", [:make, :rebar], [], "hexpm"}} diff --git a/test/support/repo.ex b/test/support/repo.ex index 594ee90..92d2244 100644 --- a/test/support/repo.ex +++ b/test/support/repo.ex @@ -1,3 +1,3 @@ defmodule Rummage.Ecto.Repo do - use Ecto.Repo, otp_app: :rummage_ecto + use Ecto.Repo, otp_app: :rummage_ecto, adapter: Sqlite.Ecto2 end From e026ebc8a5169e40dde31802e57659210bd660b8 Mon Sep 17 00:00:00 2001 From: Adi Date: Mon, 18 Dec 2017 11:13:20 -0500 Subject: [PATCH 07/64] Return of the __using__ macro --- lib/rummage_ecto.ex | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/lib/rummage_ecto.ex b/lib/rummage_ecto.ex index 9f77821..a8a8ba7 100644 --- a/lib/rummage_ecto.ex +++ b/lib/rummage_ecto.ex @@ -98,7 +98,7 @@ defmodule Rummage.Ecto do """ @spec rummage(Ecto.Query.t, map, map) :: {Ecto.Query.t, map} - def rummage(queryable, rummage, opts \\ %{}) + def rummage(queryable, rummage, opts \\ []) def rummage(queryable, rummage, _opts) when rummage == nil, do: {queryable, %{}} def rummage(queryable, rummage, opts) do hooks = opts[:hooks] || [:search, :sort, :paginate] @@ -111,4 +111,12 @@ defmodule Rummage.Ecto do {q |> hook_module.run(rummage), rummage} end) end + + defmacro __using__(_opts) do + quote do + require Rummage.Ecto + + defdelegate rummage(queryable, rummage, opts \\ []), to: Rummage.Ecto + end + end end From 3e0509e6281d9a6257f9c195f90d810c3e5b6fd5 Mon Sep 17 00:00:00 2001 From: Adi Date: Mon, 18 Dec 2017 11:16:38 -0500 Subject: [PATCH 08/64] Add tests for __using__ macro --- test/rummage_ecto_test.exs | 2 +- test/support/category.ex | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/test/rummage_ecto_test.exs b/test/rummage_ecto_test.exs index f93a6a1..57a7eab 100644 --- a/test/rummage_ecto_test.exs +++ b/test/rummage_ecto_test.exs @@ -64,7 +64,7 @@ defmodule Rummage.EctoTest do }, } - {queryable, rummage} = Rummage.Ecto.rummage(Category, rummage, per_page: 3) + {queryable, rummage} = Category.rummage(Category, rummage, per_page: 3) categories = Repo.all(queryable) diff --git a/test/support/category.ex b/test/support/category.ex index ee55ddf..e928e0e 100644 --- a/test/support/category.ex +++ b/test/support/category.ex @@ -1,5 +1,6 @@ defmodule Rummage.Ecto.Category do use Ecto.Schema + use Rummage.Ecto schema "categories" do field :category_name, :string From e7ddacfaee7d71e8ab8c6e3e829a949a8fafd6ab Mon Sep 17 00:00:00 2001 From: Adi Date: Mon, 18 Dec 2017 11:17:51 -0500 Subject: [PATCH 09/64] Fix otp verions usable by travis --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index e8acd95..d7972f7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,7 +2,7 @@ language: elixir elixir: - 1.4.5 otp_release: - - 20.2 + - 20.1 sudo: false env: - MIX_ENV=test ECTO_PGSQL_USER=postgres ECTO_PGSQL_PASSWORD= From 83302fdf0da3ee9ffda1dcec48a65cd91987f993 Mon Sep 17 00:00:00 2001 From: Adi Date: Thu, 21 Dec 2017 10:29:55 -0500 Subject: [PATCH 10/64] Change default_#{key} to #{key} --- lib/rummage_ecto/config.ex | 40 +++++++++++++++++++------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/lib/rummage_ecto/config.ex b/lib/rummage_ecto/config.ex index e056df1..25c09c2 100644 --- a/lib/rummage_ecto/config.ex +++ b/lib/rummage_ecto/config.ex @@ -5,78 +5,78 @@ defmodule Rummage.Ecto.Config do """ @doc """ - `:default_search` hook can also be set at run time + `:search` hook can also be set at run time in the `config.exs` file ## Examples When no config is set, if returns the default hook (`Rummage.Ecto.Hooks.Search`): iex> alias Rummage.Ecto.Config - iex> Config.default_search + iex> Config.search Rummage.Ecto.Hooks.Search """ - def default_search do - config(:default_search, Rummage.Ecto.Hooks.Search) + def search do + config(:search, Rummage.Ecto.Hooks.Search) end @doc """ - `:default_sort` hook can also be set at run time + `:sort` hook can also be set at run time in the `config.exs` file ## Examples When no config is set, if returns the default hook (`Rummage.Ecto.Hooks.Sort`): iex> alias Rummage.Ecto.Config - iex> Config.default_sort + iex> Config.sort Rummage.Ecto.Hooks.Sort """ - def default_sort do - config(:default_sort, Rummage.Ecto.Hooks.Sort) + def sort do + config(:sort, Rummage.Ecto.Hooks.Sort) end @doc """ - `:default_paginate` hook can also be set at run time + `:paginate` hook can also be set at run time in the `config.exs` file ## Examples When no config is set, if returns the default hook (`Rummage.Ecto.Hooks.Paginate`): iex> alias Rummage.Ecto.Config - iex> Config.default_paginate + iex> Config.paginate Rummage.Ecto.Hooks.Paginate """ - def default_paginate do - config(:default_paginate, Rummage.Ecto.Hooks.Paginate) + def paginate do + config(:paginate, Rummage.Ecto.Hooks.Paginate) end @doc """ - `:default_per_page` can also be set at run time + `:per_page` can also be set at run time in the `config.exs` file ## Examples Returns default `Repo` set in the config (`2 in `rummage_ecto`'s test env): iex> alias Rummage.Ecto.Config - iex> Config.default_per_page + iex> Config.per_page 2 """ - def default_per_page do - config(:default_per_page, 10) + def per_page do + config(:per_page, 10) end @doc """ - `:default_repo` can also be set at run time + `:repo` can also be set at run time in the config.exs file ## Examples Returns default `Repo` set in the config (`Rummage.Ecto.Repo` in `rummage_ecto`'s test env): iex> alias Rummage.Ecto.Config - iex> Config.default_repo + iex> Config.repo Rummage.Ecto.Repo """ - def default_repo do - config(:default_repo, nil) + def repo do + config(:repo, nil) end @doc """ From f0a04f10b0af798c33fc29bbe46e68881adb479c Mon Sep 17 00:00:00 2001 From: Adi Date: Fri, 5 Jan 2018 20:16:40 -0500 Subject: [PATCH 11/64] Minor updates --- lib/rummage_ecto.ex | 16 +++++++++------- mix.exs | 4 ++-- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/lib/rummage_ecto.ex b/lib/rummage_ecto.ex index a8a8ba7..900b668 100644 --- a/lib/rummage_ecto.ex +++ b/lib/rummage_ecto.ex @@ -97,19 +97,21 @@ defmodule Rummage.Ecto do """ + @default_hooks [search: :default, sort: :default, paginate: :default] + @spec rummage(Ecto.Query.t, map, map) :: {Ecto.Query.t, map} def rummage(queryable, rummage, opts \\ []) def rummage(queryable, rummage, _opts) when rummage == nil, do: {queryable, %{}} def rummage(queryable, rummage, opts) do - hooks = opts[:hooks] || [:search, :sort, :paginate] - - Enum.reduce(hooks, {queryable, rummage}, fn(hook, {q, r}) -> - hook_module = opts[hook] || apply(Config, String.to_atom("default_#{hook}"), []) + hooks = opts[:hooks] || @default_hooks + Enum.reduce(hooks, {queryable, rummage}, &apply_mod(&1, &2, opts)) + end - rummage = hook_module.before_hook(q, r, opts) + defp apply_mod({type, mod}, {queryable, rummage}, opts) do + mod = mod == :default && apply(Config, type, []) || mod - {q |> hook_module.run(rummage), rummage} - end) + {apply(mod, :run, [queryable, rummage]), + apply(mod, :before_hook, [queryable, rummage, opts])} end defmacro __using__(_opts) do diff --git a/mix.exs b/mix.exs index b920167..0fedb0d 100644 --- a/mix.exs +++ b/mix.exs @@ -1,14 +1,14 @@ defmodule Rummage.Ecto.Mixfile do use Mix.Project - @version "1.3.0-rc" + @version "1.3.0-rc.0" @url "https://github.com/aditya7iyengar/rummage_ecto" def project do [ app: :rummage_ecto, version: @version, - elixir: "~> 1.4.5", + elixir: "~> 1.4", deps: deps(), build_embedded: Mix.env == :prod, start_permanent: Mix.env == :prod, From 01a33f45e957deda29788080fb9ecab7ef3395c6 Mon Sep 17 00:00:00 2001 From: Adi Date: Mon, 8 Jan 2018 00:08:37 -0500 Subject: [PATCH 12/64] Updates to hook: - Add a __using__ macro for before enforcing the callbacks - Update callback function names and typespecs - Update documentation --- lib/rummage_ecto/hook.ex | 67 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 62 insertions(+), 5 deletions(-) diff --git a/lib/rummage_ecto/hook.ex b/lib/rummage_ecto/hook.ex index e3f5b8f..134b3f7 100644 --- a/lib/rummage_ecto/hook.ex +++ b/lib/rummage_ecto/hook.ex @@ -1,9 +1,66 @@ defmodule Rummage.Ecto.Hook do @moduledoc """ - This module defines a behavior that `Rummage.Hooks` have to follow. - Custom Search, Sort and Paginate hooks should follow this behavior - as well. + This module defines a behaviour that `Rummage.Ecto.Hook`s have to follow. + + This module also defines a `__using__` macro which mandates certain + behaviours for a `Hook` module to follow. + + Native hooks that come with `Rummage.Ecto` follow this behaviour. + + Custom Search, Sort and Paginate hooks should follow this behaviour + as well, in order for them to work well with `Rummage.Ecto` + + ## Usage + + - This is the preferred way of creating a Custom Hook. Using + `Rummage.Ecto.Hook.__using__/1` macro, it can be ensured that `run/2` and + `format_params/2` functions have been implemented. + + ```ex + defmodule MyCustomHook do + use Rummage.Ecto.Hook + + def run(queryable, params), do: queryable + + def format_params(querable, params, opts), do: params + end + ``` + + - A Custom Hook can also be created by using `Rummage.Ecto.Hook` `@behviour` + + ```ex + defmodule MyCustomHook do + @behviour Rummage.Ecto.Hook + + def run(queryable, params), do: queryable + + def format_params(querable, params, opts), do: params + end + ``` + """ - @callback run(queryable :: Ecto.Query.t, rummage :: map) :: queryable :: Ecto.Query.t - @callback before_hook(queryable :: Ecto.Query.t, rummage :: map, opts :: map) :: rummage :: map + + @callback run(Ecto.Query.t(), map()) :: Ecto.Query.t() + @callback format_params(Ecto.Query.t(), map(), keyword()) :: map() + + @doc """ + TODO: Improve Docs + """ + defmacro __using__(_opts) do + quote do + @behviour unquote(__MODULE__) + + @spec run(Ecto.Query.t(), map()) :: Ecto.Query.t() + def run(queryable, params) do + raise "run/2 not implemented for hook: #{__MODULE__}" + end + + @spec format_params(Ecto.Query.t(), map(), keyword()) :: map() + def format_params(queryable, params, opts) do + raise "format_params/2 not implemented for hook: #{__MODULE__}" + end + + defoverridable [run: 2, format_params: 3] + end + end end From 7df78e98207eea2911c1c89231fd8d87e96bb4cc Mon Sep 17 00:00:00 2001 From: Adi Date: Mon, 8 Jan 2018 00:09:35 -0500 Subject: [PATCH 13/64] Update paginate hook: - Use new hook module - Update documentation to reflect the above changes - Fix the error with primary key - Fix errors with other functions - Remove config references - Remove string key names for clarity --- lib/rummage_ecto/hooks/paginate.ex | 371 ++++++++++++++++++++--------- 1 file changed, 253 insertions(+), 118 deletions(-) diff --git a/lib/rummage_ecto/hooks/paginate.ex b/lib/rummage_ecto/hooks/paginate.ex index 0c0389e..ffb3279 100644 --- a/lib/rummage_ecto/hooks/paginate.ex +++ b/lib/rummage_ecto/hooks/paginate.ex @@ -1,7 +1,27 @@ defmodule Rummage.Ecto.Hooks.Paginate do @moduledoc """ - `Rummage.Ecto.Hooks.Paginate` is the default pagination hook that comes shipped - with `Rummage.Ecto`. + `Rummage.Ecto.Hooks.Paginate` is the default pagination hook that comes with + `Rummage.Ecto`. + + This module provides a operations that can add pagination functionality to + a pipeline of `Ecto` queries. This module works by taking a `per_page`, which + it uses to add a `limit` to the query and by setting the `offset` using the + `page` variable, which signifies the current page of entries to be displayed. + + + NOTE: This module doesn't return a list of entries, but a `Ecto.Query.t`. + + + This module `uses` `Rummage.Ecto.Hook`. + + ## Usage: + To add pagination to a `Ecto.Queryable`, simply do the following: + + ```ex + Rummage.Ecto.Hooks.Paginate.run(queryable, %{per_page: 10, page: 2}) + ``` + + ## Overriding: This module can be overridden with a custom module while using `Rummage.Ecto` in `Ecto` struct module. @@ -17,7 +37,7 @@ defmodule Rummage.Ecto.Hooks.Paginate do ```elixir config :rummage_ecto, Rummage.Ecto, - default_paginate: CustomHook + .paginate: CustomHook ``` The `CustomHook` must implement behaviour `Rummage.Ecto.Hook`. For examples of `CustomHook`, check out some @@ -25,182 +45,297 @@ defmodule Rummage.Ecto.Hooks.Paginate do Rummage.Ecto.CustomHooks.SimplePaginate """ + use Rummage.Ecto.Hook + import Ecto.Query - alias Rummage.Ecto.Config - @behaviour Rummage.Ecto.Hook + @expected_keys ~w(per_page page)a + @err_msg "Error in params, No values given for keys: " @doc """ - Builds a paginate queryable on top of the given `queryable` from the rummage parameters - from the given `rummage` struct. + This is the callback implementation of `Rummage.Ecto.Hook.run/2`. + + Builds a paginate `Ecto.Query.t` on top of a given `Ecto.Query.t` variable + with given `params`. + + Besides an `Ecto.Query.t` an `Ecto.Schema` module can also be passed as it + implements `Ecto.Queryable` + + Params is a `Map` which is expected to have the keys `#{Enum.join(@expected_keys, ", ")}`. + + If an expected key isn't given, a `Runtime Error` is raised. ## Examples - When rummage struct passed doesn't have the key "paginate", it simply returns the - queryable itself: + When an empty map is passed as `params`: iex> alias Rummage.Ecto.Hooks.Paginate iex> import Ecto.Query iex> Paginate.run(Parent, %{}) - Parent + ** (RuntimeError) Error in params, No values given for keys: per_page, page - When the queryable passed is not just a struct: + When a non-empty map is passed as `params`, but with a missing key: iex> alias Rummage.Ecto.Hooks.Paginate iex> import Ecto.Query - iex> queryable = from u in "parents" - #Ecto.Query - iex> Paginate.run(queryable, %{}) - #Ecto.Query + iex> Paginate.run(Parent, %{per_page: 10}) + ** (RuntimeError) Error in params, No values given for keys: page - When rummage `struct` passed has the key `"paginate"`, but with a value of `%{}`, `""` - or `[]` it simply returns the `queryable` itself: + When a valid map of params is passed with an `Ecto.Schema` module: iex> alias Rummage.Ecto.Hooks.Paginate iex> import Ecto.Query - iex> Paginate.run(Parent, %{"paginate" => %{}}) - Parent + iex> Paginate.run(Rummage.Ecto.Product, %{per_page: 10, page: 1}) + #Ecto.Query - iex> alias Rummage.Ecto.Hooks.Paginate - iex> import Ecto.Query - iex> Paginate.run(Parent, %{"paginate" => ""}) - Parent + When the `queryable` passed is an `Ecto.Query` variable: iex> alias Rummage.Ecto.Hooks.Paginate iex> import Ecto.Query - iex> Paginate.run(Parent, %{"paginate" => []}) - Parent + iex> queryable = from u in "products" + #Ecto.Query + iex> Paginate.run(queryable, %{per_page: 10, page: 2}) + #Ecto.Query - When rummage struct passed has the key "paginate", with "per_page" and "page" keys - it returns a paginated version of the queryable passed in as the argument: - iex> alias Rummage.Ecto.Hooks.Paginate - iex> import Ecto.Query - iex> rummage = %{"paginate" => %{"per_page" => "1", "page" => "1"}} - %{"paginate" => %{"page" => "1", "per_page" => "1"}} - iex> queryable = from u in "parents" - #Ecto.Query - iex> Paginate.run(queryable, rummage) - #Ecto.Query + More examples: iex> alias Rummage.Ecto.Hooks.Paginate iex> import Ecto.Query - iex> rummage = %{"paginate" => %{"per_page" => "5", "page" => "2"}} - %{"paginate" => %{"page" => "2", "per_page" => "5"}} - iex> queryable = from u in "parents" - #Ecto.Query + iex> rummage = %{per_page: 1, page: 1} + iex> queryable = from u in "products" + #Ecto.Query iex> Paginate.run(queryable, rummage) - #Ecto.Query - - When no `"page"` key is passed, it defaults to `1`: + #Ecto.Query iex> alias Rummage.Ecto.Hooks.Paginate iex> import Ecto.Query - iex> rummage = %{"paginate" => %{"per_page" => "10"}} - %{"paginate" => %{"per_page" => "10"}} - iex> queryable = from u in "parents" - #Ecto.Query + iex> rummage = %{per_page: 5, page: 2} + iex> queryable = from u in "products" + #Ecto.Query iex> Paginate.run(queryable, rummage) - #Ecto.Query + #Ecto.Query + """ - @spec run(Ecto.Query.t, map) :: {Ecto.Query.t, map} - def run(queryable, rummage) do - paginate_params = Map.get(rummage, "paginate") + @spec run(Ecto.Query.t, map) :: Ecto.Query.t + def run(queryable, paginate_params) do + :ok = validate_params(paginate_params) + + handle_paginate(queryable, paginate_params) + end + + # Helper function which handles addition of paginated query on top of + # the sent queryable variable + defp handle_paginate(queryable, paginate_params) do + per_page = Map.get(paginate_params, :per_page) + page = Map.get(paginate_params, :page) + offset = per_page * (page - 1) - case paginate_params do - a when a in [nil, [], {}, [""], "", %{}] -> queryable - _ -> handle_paginate(queryable, paginate_params) + queryable + |> limit(^per_page) + |> offset(^offset) + end + + # Helper function that validates the list of params based on + # @expected_keys list + defp validate_params(params) do + key_validations = Enum.map(@expected_keys, &Map.fetch(params, &1)) + + case Enum.filter(key_validations, & &1 == :error) do + [] -> :ok + _ -> raise @err_msg <> missing_keys(key_validations) end end + # Helper function used to build error message using missing keys + defp missing_keys(key_validations) do + key_validations + |> Enum.with_index() + |> Enum.filter(fn {v, _i} -> v == :error end) + |> Enum.map(fn {_v, i} -> Enum.at(@expected_keys, i) end) + |> Enum.map(&to_string/1) + |> Enum.join(", ") + end + @doc """ - Implementation of `before_hook` for `Rummage.Ecto.Hooks.Paginate`. This function - takes a `queryable`, `rummage` struct and an `opts` map. Using those it calculates - the `total_count` and `max_page` for the paginate hook. + Callback implementation for `Rummage.Ecto.Hook.format_params/2`. + + This function takes an `Ecto.Query.t` or `queryable`, `paginate_params` which + will be passed to the `run/2` function, but also takes a list of options, + `opts`. + + The function expects `opts` to include a `repo` key which points to the + `Ecto.Repo` which will be used to calculate the `total_count` and `max_page` + for this paginate hook module. + ## Examples + + When a `repo` isn't passed in `opts`: + iex> alias Rummage.Ecto.Hooks.Paginate iex> alias Rummage.Ecto.Category - iex> Paginate.before_hook(Category, %{}, %{}) - %{} + iex> Paginate.format_params(Category, %{per_page: 1, page: 1}, []) + ** (RuntimeError) Expected key `repo` in `opts`, got [] + + When `paginate_params` given aren't valid: iex> alias Rummage.Ecto.Hooks.Paginate iex> alias Rummage.Ecto.Category - iex> Ecto.Adapters.SQL.Sandbox.checkout(Rummage.Ecto.Repo) - iex> Rummage.Ecto.Repo.insert(%Category{category_name: "Category 1"}) - iex> Rummage.Ecto.Repo.insert(%Category{category_name: "Category 2"}) - iex> Rummage.Ecto.Repo.insert(%Category{category_name: "Category 3"}) - iex> rummage = %{"paginate" => %{"per_page" => "1", "page" => "1"}} - iex> Paginate.before_hook(Category, rummage, %{}) - %{"paginate" => %{"max_page" => "3", "page" => "1", "per_page" => "1", "total_count" => "3"}} - """ - @spec before_hook(Ecto.Query.t, map, map) :: map - def before_hook(queryable, rummage, opts) do - paginate_params = Map.get(rummage, "paginate") + iex> Paginate.format_params(Category, %{}, []) + ** (RuntimeError) Error in params, No values given for keys: per_page, page - case paginate_params do - nil -> rummage - _ -> - total_count = get_total_count(queryable, opts) + When `paginate_params` and `opts` given are valid: - {page, per_page} = parse_page_and_per_page(paginate_params, opts) + iex> alias Rummage.Ecto.Hooks.Paginate + iex> alias Rummage.Ecto.Category + iex> paginate_params = %{ + ...> per_page: 1, + ...> page: 1 + ...> } + iex> repo = Rummage.Ecto.Repo + iex> Ecto.Adapters.SQL.Sandbox.checkout(repo) + iex> Paginate.format_params(Category, paginate_params, [repo: repo]) + %{max_page: 0, page: 1, per_page: 1, total_count: 0} - per_page = (per_page < 1) && 1 || per_page + When `paginate_params` and `opts` given are valid: - max_page_fl = total_count / per_page - max_page = max_page_fl - |> Float.ceil - |> round + iex> alias Rummage.Ecto.Hooks.Paginate + iex> alias Rummage.Ecto.Category + iex> paginate_params = %{ + ...> per_page: 1, + ...> page: 1 + ...> } + iex> repo = Rummage.Ecto.Repo + iex> Ecto.Adapters.SQL.Sandbox.checkout(repo) + iex> repo.insert!(%Category{category_name: "name"}) + iex> repo.insert!(%Category{category_name: "name2"}) + iex> Paginate.format_params(Category, paginate_params, [repo: repo]) + %{max_page: 2, page: 1, per_page: 1, total_count: 2} + + When `paginate_params` and `opts` given are valid and when the `queryable` + passed has a `primary_key` defaulted to `id`. + + iex> alias Rummage.Ecto.Hooks.Paginate + iex> alias Rummage.Ecto.Category + iex> paginate_params = %{ + ...> per_page: 1, + ...> page: 1 + ...> } + iex> repo = Rummage.Ecto.Repo + iex> Ecto.Adapters.SQL.Sandbox.checkout(repo) + iex> repo.insert!(%Category{category_name: "name"}) + iex> repo.insert!(%Category{category_name: "name2"}) + iex> Paginate.format_params(Category, paginate_params, [repo: repo]) + %{max_page: 2, page: 1, per_page: 1, total_count: 2} + + When `paginate_params` and `opts` given are valid and when the `queryable` + passed has a custom `primary_key`. - page = cond do - page < 1 -> 1 - max_page > 0 && page > max_page -> max_page - true -> page - end + iex> alias Rummage.Ecto.Hooks.Paginate + iex> alias Rummage.Ecto.Item + iex> paginate_params = %{ + ...> per_page: 1, + ...> page: 1 + ...> } + iex> repo = Rummage.Ecto.Repo + iex> Ecto.Adapters.SQL.Sandbox.checkout(repo) + iex> repo.insert!(%Item{item_id: 5}) + iex> repo.insert!(%Item{item_id: 6}) + iex> Paginate.format_params(Item, paginate_params, [repo: repo]) + %{max_page: 2, page: 1, per_page: 1, total_count: 2} + + When `paginate_params` and `opts` given are valid and when the `queryable` + passed has a custom `primary_key`. - paginate_params = paginate_params - |> Map.put("page", Integer.to_string(page)) - |> Map.put("per_page", Integer.to_string(per_page)) - |> Map.put("total_count", Integer.to_string(total_count)) - |> Map.put("max_page", Integer.to_string(max_page)) + iex> alias Rummage.Ecto.Hooks.Paginate + iex> alias Rummage.Ecto.Nopk + iex> paginate_params = %{ + ...> per_page: 1, + ...> page: 1 + ...> } + iex> repo = Rummage.Ecto.Repo + iex> Ecto.Adapters.SQL.Sandbox.checkout(repo) + iex> repo.insert!(%Nopk{field: 5.0}) + iex> repo.insert!(%Nopk{field: 6.0}) + iex> Paginate.format_params(Nopk, paginate_params, [repo: repo]) + %{max_page: 2, page: 1, per_page: 1, total_count: 2} + + When `paginate_params` and `opts` given are valid and when the `queryable` + passed is not a `Ecto.Schema` module, but an `Ecto.Query.t`. - Map.put(rummage, "paginate", paginate_params) + iex> alias Rummage.Ecto.Hooks.Paginate + iex> alias Rummage.Ecto.Nopk + iex> paginate_params = %{ + ...> per_page: 1, + ...> page: 1 + ...> } + iex> repo = Rummage.Ecto.Repo + iex> Ecto.Adapters.SQL.Sandbox.checkout(repo) + iex> repo.insert!(%Nopk{field: 5.0}) + iex> repo.insert!(%Nopk{field: 6.0}) + iex> import Ecto.Query + iex> queryable = from u in Nopk, where: u.field > 5.0 + iex> Paginate.format_params(queryable, paginate_params, [repo: repo]) + %{max_page: 1, page: 1, per_page: 1, total_count: 1} + + """ + @spec format_params(Ecto.Query.t(), map(), keyword()) :: map() + def format_params(queryable, paginate_params, opts) do + :ok = validate_params(paginate_params) + + case Keyword.get(opts, :repo) do + nil -> raise "Expected key `repo` in `opts`, got #{inspect(opts)}" + repo -> get_params(queryable, paginate_params, repo) end end - defp get_total_count(queryable, opts) do - subquery = from s in subquery(queryable), select: count(s.id) - hd(apply(get_repo(opts), :all, [subquery])) + # Helper function which gets formatted list of params including + # page, per_page, total_count and max_page keys + defp get_params(queryable, paginate_params, repo) do + per_page = Map.get(paginate_params, :per_page) + total_count = get_total_count(queryable, repo) + max_page = (total_count / per_page) + |> Float.ceil() + |> trunc() + + %{page: Map.get(paginate_params, :page), + per_page: per_page, total_count: total_count, max_page: max_page} end - defp get_repo(opts) do - opts[:repo] || Config.default_repo + # Helper function which gets total count of a queryable based on + # the given repo. + # This excludes operations such as select, preload and order_by + # to make the query more effectient + defp get_total_count(queryable, repo) do + queryable + |> exclude(:select) + |> exclude(:preload) + |> exclude(:order_by) + |> get_count(repo, pk(queryable)) end - defp parse_page_and_per_page(paginate_params, opts) do - per_page = paginate_params - |> Map.get("per_page", Integer.to_string(opts[:per_page] || Config.default_per_page)) - |> String.to_integer - - page = paginate_params - |> Map.get("page", "1") - |> String.to_integer - - {page, per_page} + # This function gets count of a query and repo passed. + # When primary key passed is nil, it just gets all the elements + # and counts them, but when a primary key is passed it just counts + # the distinct primary keys + defp get_count(query, repo, nil) do + repo + |> apply(:all, [distinct(query, :true)]) + |> Enum.count() + end + defp get_count(query, repo, pk) do + query = select(query, [s], count(field(s, ^pk), :distinct)) + hd(apply(repo, :all, [query])) end - defp handle_paginate(queryable, paginate_params) do - per_page = paginate_params - |> Map.get("per_page") - |> String.to_integer - - page = paginate_params - |> Map.get("page", "1") - |> String.to_integer - - offset = per_page * (page - 1) + # Helper function which returns the primary key associated with a + # Queryable. + defp pk(queryable) do + schema = is_map(queryable) && elem(queryable.from, 1) || queryable - queryable - |> limit(^per_page) - |> offset(^offset) + case schema.__schema__(:primary_key) do + [] -> nil + list -> hd(list) + end end end From 965d352415090e64318bffc9459f93aecc2bd757 Mon Sep 17 00:00:00 2001 From: Adi Date: Mon, 8 Jan 2018 00:11:05 -0500 Subject: [PATCH 14/64] Add items for testing: - Add schema for Item - This is for testing a Schema with a custom primary key - Add migration for items --- .../migrations/20180103043059_create_items.exs | 13 +++++++++++++ test/support/item.ex | 17 +++++++++++++++++ 2 files changed, 30 insertions(+) create mode 100644 priv/repo/migrations/20180103043059_create_items.exs create mode 100644 test/support/item.ex diff --git a/priv/repo/migrations/20180103043059_create_items.exs b/priv/repo/migrations/20180103043059_create_items.exs new file mode 100644 index 0000000..c6432b1 --- /dev/null +++ b/priv/repo/migrations/20180103043059_create_items.exs @@ -0,0 +1,13 @@ +defmodule Rummage.Ecto.Repo.Migrations.CreateItems do + use Ecto.Migration + + def change do + create table(:items, primary_key: false) do + add :item_id, :id, primary_key: true + add :item_price, :float + add :category_id, references(:categories) + + timestamps() + end + end +end diff --git a/test/support/item.ex b/test/support/item.ex new file mode 100644 index 0000000..1e34d73 --- /dev/null +++ b/test/support/item.ex @@ -0,0 +1,17 @@ +defmodule Rummage.Ecto.Item do + @moduledoc """ + This module was created to test Rummage.Ecto with a Schema that doesn't + have a primary_key of :id. + """ + + use Ecto.Schema + + @primary_key {:item_id, :id, autogenerate: false} + + schema "items" do + field :item_price, :float + belongs_to :category, Rummage.Ecto.Category + + timestamps() + end +end From 59c4b971d291b6395fe6a8f17a0c3024eefb4a9f Mon Sep 17 00:00:00 2001 From: Adi Date: Mon, 8 Jan 2018 00:11:50 -0500 Subject: [PATCH 15/64] Add nopk for testing: - Add schema for Nopk - This is for testing a Schema without a primary key - Add migration for nopks --- .../migrations/20180104043059_create_nopks.exs | 11 +++++++++++ test/support/nopk.ex | 16 ++++++++++++++++ 2 files changed, 27 insertions(+) create mode 100644 priv/repo/migrations/20180104043059_create_nopks.exs create mode 100644 test/support/nopk.ex diff --git a/priv/repo/migrations/20180104043059_create_nopks.exs b/priv/repo/migrations/20180104043059_create_nopks.exs new file mode 100644 index 0000000..860d630 --- /dev/null +++ b/priv/repo/migrations/20180104043059_create_nopks.exs @@ -0,0 +1,11 @@ +defmodule Rummage.Ecto.Repo.Migrations.CreateNopks do + use Ecto.Migration + + def change do + create table(:nopks, primary_key: false) do + add :field, :float + + timestamps() + end + end +end diff --git a/test/support/nopk.ex b/test/support/nopk.ex new file mode 100644 index 0000000..a5cdf17 --- /dev/null +++ b/test/support/nopk.ex @@ -0,0 +1,16 @@ +defmodule Rummage.Ecto.Nopk do + @moduledoc """ + This module was created to test Rummage.Ecto with a Schema that doesn't + have any primary_key. + """ + + use Ecto.Schema + + @primary_key false + + schema "nopks" do + field :field, :float + + timestamps() + end +end From 1f69206c4cad08973c161ac2eb4513a82a439cce Mon Sep 17 00:00:00 2001 From: Adi Date: Mon, 8 Jan 2018 01:20:21 -0500 Subject: [PATCH 16/64] Add credo fixes: - Pipechain should start with raw values - Update docs with better syntax --- lib/rummage_ecto/hooks/paginate.ex | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/rummage_ecto/hooks/paginate.ex b/lib/rummage_ecto/hooks/paginate.ex index ffb3279..75f8caa 100644 --- a/lib/rummage_ecto/hooks/paginate.ex +++ b/lib/rummage_ecto/hooks/paginate.ex @@ -40,8 +40,9 @@ defmodule Rummage.Ecto.Hooks.Paginate do .paginate: CustomHook ``` - The `CustomHook` must implement behaviour `Rummage.Ecto.Hook`. For examples of `CustomHook`, check out some - `custom_hooks` that are shipped with elixir: `Rummage.Ecto.CustomHooks.SimpleSearch`, `Rummage.Ecto.CustomHooks.SimpleSort`, + The `CustomHook` must use `Rummage.Ecto.Hook`. For examples of `CustomHook`, + check out some `custom_hooks` that are shipped with `Rummage.Ecto`: + `Rummage.Ecto.CustomHooks.SimpleSearch`, `Rummage.Ecto.CustomHooks.SimpleSort`, Rummage.Ecto.CustomHooks.SimplePaginate """ @@ -294,7 +295,8 @@ defmodule Rummage.Ecto.Hooks.Paginate do defp get_params(queryable, paginate_params, repo) do per_page = Map.get(paginate_params, :per_page) total_count = get_total_count(queryable, repo) - max_page = (total_count / per_page) + max_page = total_count + |> (& &1 / per_page).() |> Float.ceil() |> trunc() From c757cf0a88ac43150e2cedf339f6617930d10dce Mon Sep 17 00:00:00 2001 From: Adi Date: Mon, 8 Jan 2018 01:20:38 -0500 Subject: [PATCH 17/64] Update Search Hook: - Add documentation with new changes - Update doc tests - Make search hook clean and extendible --- lib/rummage_ecto/hooks/search.ex | 353 +++++++++++-------------------- 1 file changed, 126 insertions(+), 227 deletions(-) diff --git a/lib/rummage_ecto/hooks/search.ex b/lib/rummage_ecto/hooks/search.ex index 22f4628..52a87ab 100644 --- a/lib/rummage_ecto/hooks/search.ex +++ b/lib/rummage_ecto/hooks/search.ex @@ -1,10 +1,19 @@ defmodule Rummage.Ecto.Hooks.Search do @moduledoc """ - `Rummage.Ecto.Hooks.Search` is the default search hook that comes shipped - with `Rummage.Ecto`. + `Rummage.Ecto.Hooks.Search` is the default search hook that comes with + `Rummage.Ecto`. - This module can be overridden with a custom module while using `Rummage.Ecto` - in `Ecto` struct module. + This module provides a operations that can add searching functionality to + a pipeline of `Ecto` queries. This module works by taking fields, and `search_type`, + `search_term` and `assoc` associated with those `fields`. + + TODO: Explain how to use `assoc` better + + + NOTE: This module doesn't return a list of entries, but a `Ecto.Query.t`. + + + This module `uses` `Rummage.Ecto.Hook`. Usage: For a regular search: @@ -15,7 +24,7 @@ defmodule Rummage.Ecto.Hooks.Search do ```elixir alias Rummage.Ecto.Hooks.Search - searched_queryable = Search.run(Parent, %{"search" => %{"field_1" => %{"assoc" => [], "search_type" => "like", "search_term" => "field_!"}}}) + searched_queryable = Search.run(Parent, %{field_1: %{assoc: [], search_type: "like", search_term: "field_!"}}}) ``` @@ -29,7 +38,7 @@ defmodule Rummage.Ecto.Hooks.Search do ```elixir alias Rummage.Ecto.Hooks.Search - searched_queryable = Search.run(Parent, %{"search" => %{"field_1" => %{"assoc" => [], "search_type" => "ilike", "search_term" => "field_!"}}}) + searched_queryable = Search.run(Parent, %{field_1: %{assoc: [], search_type: "ilike", search_term: "field_!"}}}) ``` @@ -50,276 +59,166 @@ defmodule Rummage.Ecto.Hooks.Search do ```elixir config :rummage_ecto, Rummage.Ecto, - default_search: CustomHook + .search: CustomHook ``` - The `CustomHook` must implement `behaviour `Rummage.Ecto.Hook`. For examples of `CustomHook`, check out some - `custom_hooks` that are shipped with elixir: `Rummage.Ecto.CustomHooks.SimpleSearch`, `Rummage.Ecto.CustomHooks.SimpleSort`, + The `CustomHook` must use `Rummage.Ecto.Hook`. For examples of `CustomHook`, + check out some `custom_hooks` that are shipped with `Rummage.Ecto`: + `Rummage.Ecto.CustomHooks.SimpleSearch`, `Rummage.Ecto.CustomHooks.SimpleSort`, Rummage.Ecto.CustomHooks.SimplePaginate """ + use Rummage.Ecto.Hook + import Ecto.Query + @expected_keys ~w(search_type assoc search_term)a + @err_msg "Error in params, No values given for keys: " + alias Rummage.Ecto.Services.BuildSearchQuery - @behaviour Rummage.Ecto.Hook + @doc ~S""" + This is the callback implementation of `Rummage.Ecto.Hook.run/2`. - @doc """ - Builds a search queryable on top of the given `queryable` from the rummage parameters - from the given `rummage` struct. + Builds a search `Ecto.Query.t` on top of a given `Ecto.Query.t` variable + with given `params`. + + Besides an `Ecto.Query.t` an `Ecto.Schema` module can also be passed as it + implements `Ecto.Queryable` + + Params is a `Map`, keys of which are field names which will be searched for and + value corresponding to that key is a list of params for that key, which + should include the keys: `#{Enum.join(@expected_keys, ", ")}`. + + This function expects a `search_type` and a list of `associations` (empty for none). + The `search_term` is what the `field` will be matched to based on the + `search_type`. + + For all `search_types`, refer to `Rummage.Ecto.Services.BuildSearchQuery`. + + If an expected key isn't given, a `Runtime Error` is raised. + + NOTE:This hook isn't responsible for doing type validations. That's the + responsibility of the user sending `search_term` and `search_type`. Same + goes for the validity of `assoc`. ## Examples - When rummage struct passed doesn't have the key "search", it simply returns the - queryable itself: + When search_params are empty, it simply returns the same `queryable`: iex> alias Rummage.Ecto.Hooks.Search iex> import Ecto.Query iex> Search.run(Parent, %{}) Parent - When the queryable passed is not just a struct: + When a non-empty map is passed as a field `params`, but with a missing key: iex> alias Rummage.Ecto.Hooks.Search iex> import Ecto.Query - iex> queryable = from u in "parents" - #Ecto.Query - iex> Search.run(queryable, %{}) - #Ecto.Query + iex> Search.run(Parent, %{field: %{assoc: []}}) + ** (RuntimeError) Error in params, No values given for keys: search_type, search_term - When rummage `struct` passed has the key `"search"`, but with a value of `%{}`, `""` - or `[]` it simply returns the `queryable` itself: + When a valid map of params is passed with an `Ecto.Schema` module: iex> alias Rummage.Ecto.Hooks.Search iex> import Ecto.Query - iex> Search.run(Parent, %{"search" => %{}}) - Parent + iex> search_params = %{field1: %{assoc: [], + ...> search_type: "like", search_term: "field1"}} + iex> Search.run(Rummage.Ecto.Product, search_params) + #Ecto.Query - iex> alias Rummage.Ecto.Hooks.Search - iex> import Ecto.Query - iex> Search.run(Parent, %{"search" => ""}) - Parent + When a valid map of params is passed with an `Ecto.Query.t`: iex> alias Rummage.Ecto.Hooks.Search iex> import Ecto.Query - iex> Search.run(Parent, %{"search" => %{}}) - Parent + iex> search_params = %{field1: %{assoc: [], + ...> search_type: "like", search_term: "field1"}} + iex> query = from p in "products" + iex> Search.run(query, search_params) + #Ecto.Query - When rummage `struct` passed has the key "search", with `field`, `associations` - `search_type` and `term` it returns a searched version of the `queryable` passed in - as the argument: - - When `associations` is an empty `list`: - When rummage `struct` passed has `search_type` of `like`, it returns - a searched version of the `queryable` with `like` search query: - - iex> alias Rummage.Ecto.Hooks.Search - iex> import Ecto.Query - iex> rummage = %{"search" => %{"field_1" => %{"assoc" => [], "search_type" => "like", "search_term" => "field_!"}}} - %{"search" => %{"field_1" => %{"assoc" => [], "search_type" => "like", "search_term" => "field_!"}}} - iex> queryable = from u in "parents" - #Ecto.Query - iex> Search.run(queryable, rummage) - #Ecto.Query - - When rummage `struct` passed has `search_type` of `ilike` (case insensitive), it returns - a searched version of the `queryable` with `ilike` search query: - - iex> alias Rummage.Ecto.Hooks.Search - iex> import Ecto.Query - iex> rummage = %{"search" => %{"field_1" => %{"assoc" => [], "search_type" => "ilike", "search_term" => "field_!"}}} - %{"search" => %{"field_1" => %{"assoc" => [], "search_type" => "ilike", "search_term" => "field_!"}}} - iex> queryable = from u in "parents" - #Ecto.Query - iex> Search.run(queryable, rummage) - #Ecto.Query - - When rummage `struct` passed has `search_type` of `eq`, it returns - a searched version of the `queryable` with `==` search query: - - iex> alias Rummage.Ecto.Hooks.Search - iex> import Ecto.Query - iex> rummage = %{"search" => %{"field_1" => %{"assoc" => [], "search_type" => "eq", "search_term" => "field_!"}}} - %{"search" => %{"field_1" => %{"assoc" => [], "search_type" => "eq", "search_term" => "field_!"}}} - iex> queryable = from u in "parents" - #Ecto.Query - iex> Search.run(queryable, rummage) - #Ecto.Query - - When rummage `struct` passed has `search_type` of `gt`, it returns - a searched version of the `queryable` with `>` search query: - - iex> alias Rummage.Ecto.Hooks.Search - iex> import Ecto.Query - iex> rummage = %{"search" => %{"field_1" => %{"assoc" => [], "search_type" => "gt", "search_term" => "field_!"}}} - %{"search" => %{"field_1" => %{"assoc" => [], "search_type" => "gt", "search_term" => "field_!"}}} - iex> queryable = from u in "parents" - #Ecto.Query - iex> Search.run(queryable, rummage) - #Ecto.Query ^"field_!"> - - When rummage `struct` passed has `search_type` of `lt`, it returns - a searched version of the `queryable` with `<` search query: - - iex> alias Rummage.Ecto.Hooks.Search - iex> import Ecto.Query - iex> rummage = %{"search" => %{"field_1" => %{"assoc" => [], "search_type" => "lt", "search_term" => "field_!"}}} - %{"search" => %{"field_1" => %{"assoc" => [], "search_type" => "lt", "search_term" => "field_!"}}} - iex> queryable = from u in "parents" - #Ecto.Query - iex> Search.run(queryable, rummage) - #Ecto.Query - - When rummage `struct` passed has `search_type` of `gteq`, it returns - a searched version of the `queryable` with `>=` search query: - - iex> alias Rummage.Ecto.Hooks.Search - iex> import Ecto.Query - iex> rummage = %{"search" => %{"field_1" => %{"assoc" => [], "search_type" => "gteq", "search_term" => "field_!"}}} - %{"search" => %{"field_1" => %{"assoc" => [], "search_type" => "gteq", "search_term" => "field_!"}}} - iex> queryable = from u in "parents" - #Ecto.Query - iex> Search.run(queryable, rummage) - #Ecto.Query= ^"field_!"> - - When rummage `struct` passed has `search_type` of `lteq`, it returns - a searched version of the `queryable` with `<=` search query: - - iex> alias Rummage.Ecto.Hooks.Search - iex> import Ecto.Query - iex> rummage = %{"search" => %{"field_1" => %{"assoc" => [], "search_type" => "lteq", "search_term" => "field_!"}}} - %{"search" => %{"field_1" => %{"assoc" => [], "search_type" => "lteq", "search_term" => "field_!"}}} - iex> queryable = from u in "parents" - #Ecto.Query - iex> Search.run(queryable, rummage) - #Ecto.Query - - When `associations` is not an empty `list`: - When rummage `struct` passed has `search_type` of `like`, it returns - a searched version of the `queryable` with `like` search query: - - iex> alias Rummage.Ecto.Hooks.Search - iex> import Ecto.Query - iex> rummage = %{"search" => %{"field_1" => %{"assoc" => ["parent", "parent"], "search_type" => "like", "search_term" => "field_!"}}} - %{"search" => %{"field_1" => %{"assoc" => ["parent", "parent"], "search_type" => "like", "search_term" => "field_!"}}} - iex> queryable = from u in "parents" - #Ecto.Query - iex> Search.run(queryable, rummage) - #Ecto.Query - - When rummage `struct` passed has `search_type` of `lteq`, it returns - a searched version of the `queryable` with `<=` search query: - - iex> alias Rummage.Ecto.Hooks.Search - iex> import Ecto.Query - iex> rummage = %{"search" => %{"field_1" => %{"assoc" => ["parent", "parent"], "search_type" => "lteq", "search_term" => "field_!"}}} - %{"search" => %{"field_1" => %{"assoc" => ["parent", "parent"], "search_type" => "lteq", "search_term" => "field_!"}}} - iex> queryable = from u in "parents" - #Ecto.Query - iex> Search.run(queryable, rummage) - #Ecto.Query - - When rummage `struct` passed has an empty string as `search_term`, it returns the `queryable` itself: - - iex> alias Rummage.Ecto.Hooks.Search - iex> import Ecto.Query - iex> rummage = %{"search" => %{"field_1" => %{"assoc" => ["parent", "parent"], "search_type" => "lteq", "search_term" => ""}}} - %{"search" => %{"field_1" => %{"assoc" => ["parent", "parent"], "search_type" => "lteq", "search_term" => ""}}} - iex> queryable = from u in "parents" - #Ecto.Query - iex> Search.run(queryable, rummage) - #Ecto.Query - - When rummage `struct` passed has nil as `search_term`, it returns the `queryable` itself: - - iex> alias Rummage.Ecto.Hooks.Search - iex> import Ecto.Query - iex> rummage = %{"search" => %{"field_1" => %{"assoc" => ["parent", "parent"], "search_type" => "lteq", "search_term" => nil}}} - %{"search" => %{"field_1" => %{"assoc" => ["parent", "parent"], "search_type" => "lteq", "search_term" => nil}}} - iex> queryable = from u in "parents" - #Ecto.Query - iex> Search.run(queryable, rummage) - #Ecto.Query - - When rummage `struct` passed has an empty array as `search_term`, it returns the `queryable` itself: - - iex> alias Rummage.Ecto.Hooks.Search - iex> import Ecto.Query - iex> rummage = %{"search" => %{"field_1" => %{"assoc" => ["parent", "parent"], "search_type" => "lteq", "search_term" => []}}} - %{"search" => %{"field_1" => %{"assoc" => ["parent", "parent"], "search_type" => "lteq", "search_term" => []}}} - iex> queryable = from u in "parents" - #Ecto.Query - iex> Search.run(queryable, rummage) - #Ecto.Query - - When `associations` is an empty `string`: - When rummage `struct` passed has `search_type` of `like`, it returns - a searched version of the `queryable` with `like` search query: - - iex> alias Rummage.Ecto.Hooks.Search - iex> import Ecto.Query - iex> rummage = %{"search" => %{"field_1" => %{"assoc" => "", "search_type" => "like", "search_term" => "field_!"}}} - %{"search" => %{"field_1" => %{"assoc" => "", "search_type" => "like", "search_term" => "field_!"}}} - iex> queryable = from u in "parents" - #Ecto.Query - iex> Search.run(queryable, rummage) - #Ecto.Query - """ - @spec run(Ecto.Query.t, map) :: {Ecto.Query.t, map} - def run(queryable, rummage) do - search_params = Map.get(rummage, "search") + When a valid map of params is passed with an `Ecto.Query.t`, with `assoc`s: - case search_params do - a when a in [nil, [], {}, [""], "", %{}] -> queryable - _ -> handle_search(queryable, search_params) - end - end + iex> alias Rummage.Ecto.Hooks.Search + iex> import Ecto.Query + iex> search_params = %{field1: %{assoc: [inner: "category"], + ...> search_type: "like", search_term: "field1"}} + iex> query = from p in "products" + iex> Search.run(query, search_params) + #Ecto.Query - @doc """ - Implementation of `before_hook` for `Rummage.Ecto.Hooks.Search`. This just returns back `rummage` at this point. - It doesn't matter what `queryable` or `opts` are, it just returns back `rummage`. + When a valid map of params is passed with an `Ecto.Query.t`, with `assoc`s, with + different join types: - ## Examples iex> alias Rummage.Ecto.Hooks.Search - iex> Search.before_hook(Parent, %{}, %{}) - %{} + iex> import Ecto.Query + iex> search_params = %{field1: %{assoc: [inner: "category", left: "category", cross: "category"], + ...> search_type: "like", search_term: "field1"}} + iex> query = from p in "products" + iex> Search.run(query, search_params) + #Ecto.Query + """ - @spec before_hook(Ecto.Query.t, map, map) :: map - def before_hook(_queryable, rummage, _opts), do: rummage + @spec run(Ecto.Query.t(), map()) :: Ecto.Query.t() + def run(q, s), do: handle_search(q, s) defp handle_search(queryable, search_params) do search_params - |> Map.to_list + |> Map.to_list() |> Enum.reduce(queryable, &search_queryable(&1, &2)) end defp search_queryable(param, queryable) do - field = param - |> elem(0) - |> String.to_atom + field = elem(param, 0) + field_params = elem(param, 1) - field_params = param - |> elem(1) + :ok = validate_params(field_params) - association_names = case field_params["assoc"] do - a when a in [nil, "", []] -> [] - assoc -> assoc - end + assocs = Map.get(field_params, :assoc) + search_type = Map.get(field_params, :search_type) + search_term = Map.get(field_params, :search_term) - search_type = field_params["search_type"] - search_term = field_params["search_term"] + assocs + |> Enum.reduce(from(e in subquery(queryable)), &join_by_assoc(&1, &2)) + |> BuildSearchQuery.run(field, search_type, search_term) + end - case search_term do - s when s in [nil, "", []] -> queryable - _ -> - queryable = from(e in subquery(queryable)) + # TODO: Support other join types + defp join_by_assoc({join, assoc}, query) do + join(query, join, [..., p1], p2 in assoc(p1, ^String.to_atom(assoc))) + end + + # Helper function that validates the list of params based on + # @expected_keys list + defp validate_params(params) do + key_validations = Enum.map(@expected_keys, &Map.fetch(params, &1)) - association_names - |> Enum.reduce(queryable, &join_by_association(&1, &2)) - |> BuildSearchQuery.run(field, search_type, search_term) + case Enum.filter(key_validations, & &1 == :error) do + [] -> :ok + _ -> raise @err_msg <> missing_keys(key_validations) end end - defp join_by_association(association, queryable) do - join(queryable, :inner, [..., p1], p2 in assoc(p1, ^String.to_atom(association))) + # Helper function used to build error message using missing keys + defp missing_keys(key_validations) do + key_validations + |> Enum.with_index() + |> Enum.filter(fn {v, _i} -> v == :error end) + |> Enum.map(fn {_v, i} -> Enum.at(@expected_keys, i) end) + |> Enum.map(&to_string/1) + |> Enum.join(", ") end + + @doc """ + Callback implementation for `Rummage.Ecto.Hook.format_params/2`. + + This just returns back `search_params` at this point. + It doesn't matter what `queryable` or `opts` are. + + ## Examples + iex> alias Rummage.Ecto.Hooks.Search + iex> Search.format_params(Parent, %{}, %{}) + %{} + """ + @spec format_params(Ecto.Query.t(), map(), keyword()) :: map() + def format_params(_queryable, search_params, _opts), do: search_params end From 6e8b192347268efe88c93245ae56cd121bf178d8 Mon Sep 17 00:00:00 2001 From: Adi Date: Mon, 8 Jan 2018 01:21:40 -0500 Subject: [PATCH 18/64] Add helpful docs: - WIP --- help/nomenclature.md | 1 + help/walkthrough.md | 1 + 2 files changed, 2 insertions(+) create mode 100644 help/nomenclature.md create mode 100644 help/walkthrough.md diff --git a/help/nomenclature.md b/help/nomenclature.md new file mode 100644 index 0000000..6c21393 --- /dev/null +++ b/help/nomenclature.md @@ -0,0 +1 @@ +# Nomenclature diff --git a/help/walkthrough.md b/help/walkthrough.md new file mode 100644 index 0000000..d7e552b --- /dev/null +++ b/help/walkthrough.md @@ -0,0 +1 @@ +# Walkthrough From 6d74948283d23c949d8485f9f84c862c514a66ca Mon Sep 17 00:00:00 2001 From: Adi Date: Mon, 8 Jan 2018 01:21:57 -0500 Subject: [PATCH 19/64] Update readme with new config fields --- README.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index ebe6da4..4016480 100644 --- a/README.md +++ b/README.md @@ -70,7 +70,7 @@ This package is [available in Hex](https://hexdocs.pm/rummage_ecto/), and can be ```elixir config :rummage_ecto, Rummage.Ecto, - default_search: MyApp.SearchModule + search: MyApp.SearchModule ``` - For configuring a repo: @@ -78,12 +78,12 @@ This package is [available in Hex](https://hexdocs.pm/rummage_ecto/), and can be ```elixir config :rummage_ecto, Rummage.Ecto, - default_repo: MyApp.Repo # This can be overridden per model basis, if need be. + repo: MyApp.Repo # This can be overridden per model basis, if need be. ``` - - Other config options are: `default_repo`, `default_sort`, `default_paginate`, `default_per_page` + - Other config options are: `repo`, `sort`, `paginate`, `per_page` - - `Rummage.Ecto` can be configured globally with a `default_per_page` value (which can be overridden for a model). + - `Rummage.Ecto` can be configured globally with a `per_page` value (which can be overridden for a model). If you want to set different `per_page` for different the models, add it to `model.exs` file while using `Rummage.Ecto` as shown in the [Advanced Usage Section](#advanced-usage). @@ -99,8 +99,8 @@ Below are the ways `Rummage.Ecto` can be used: ```elixir config :rummage_ecto, Rummage.Ecto, - default_repo: MyApp.Repo, - default_per_page: 10 + repo: MyApp.Repo, + per_page: 10 ``` - And you should be able to use `Rummage.Ecto` with any `Ecto` model. @@ -112,9 +112,9 @@ Below are the ways `Rummage.Ecto` can be used: ```elixir config :rummage_ecto, Rummage.Ecto, - default_repo: MyApp.Repo, - default_search: MyApp.SearchModule, - default_paginate: MyApp.PaginateModule + repo: MyApp.Repo, + search: MyApp.SearchModule, + paginate: MyApp.PaginateModule ``` - When using `Rummage.Ecto` with an app that has multiple `Repo`s, or when there's a need to configure `Repo` per model basis, it can be passed along with From 2c533c425903a03ca6c4b0857940d58f37ecc58a Mon Sep 17 00:00:00 2001 From: Adi Date: Mon, 8 Jan 2018 01:22:11 -0500 Subject: [PATCH 20/64] Update test with new config fields --- config/test.exs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/test.exs b/config/test.exs index aba6087..00ff3a7 100644 --- a/config/test.exs +++ b/config/test.exs @@ -4,8 +4,8 @@ config :logger, :console, level: :error config :rummage_ecto, Rummage.Ecto,[ - default_repo: Rummage.Ecto.Repo, - default_per_page: 2, + repo: Rummage.Ecto.Repo, + per_page: 2, ] config :rummage_ecto, ecto_repos: [Rummage.Ecto.Repo] From ee11491066b7f3d8644b391c678315ea094e6dea Mon Sep 17 00:00:00 2001 From: Adi Date: Mon, 8 Jan 2018 01:22:38 -0500 Subject: [PATCH 21/64] Remove the much deprecated system var support --- lib/rummage_ecto/config.ex | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/lib/rummage_ecto/config.ex b/lib/rummage_ecto/config.ex index 25c09c2..1adc7ca 100644 --- a/lib/rummage_ecto/config.ex +++ b/lib/rummage_ecto/config.ex @@ -79,22 +79,6 @@ defmodule Rummage.Ecto.Config do config(:repo, nil) end - @doc """ - `resolve_system_config` returns a `system` variable set up with `var_name` key - or returns the specified `default` value. Takes in `arg` whose first element is - an atom `:system`. - - ## Examples - Returns value corresponding to a system variable config or returns the `default` value: - iex> alias Rummage.Ecto.Config - iex> Config.resolve_system_config({:system, "some random config"}, "default") - "default" - """ - @spec resolve_system_config(Tuple.t, term) :: {term} - def resolve_system_config({:system, var_name}, default) do - System.get_env(var_name) || default - end - defp config do Application.get_env(:rummage_ecto, Rummage.Ecto, []) end From 69eabf5bc0ee10409d417e6ed9d0ae145b20c197 Mon Sep 17 00:00:00 2001 From: Adi Date: Mon, 8 Jan 2018 01:23:08 -0500 Subject: [PATCH 22/64] Update sort hook docs - WIP: Still need to work on this one --- lib/rummage_ecto/hooks/sort.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/rummage_ecto/hooks/sort.ex b/lib/rummage_ecto/hooks/sort.ex index 57ef0de..e0cb6c3 100644 --- a/lib/rummage_ecto/hooks/sort.ex +++ b/lib/rummage_ecto/hooks/sort.ex @@ -43,7 +43,7 @@ defmodule Rummage.Ecto.Hooks.Sort do ```elixir config :rummage_ecto, Rummage.Ecto, - default_sort: CustomHook + .sort: CustomHook ``` The `CustomHook` must implement behaviour `Rummage.Ecto.Hook`. For examples of `CustomHook`, check out some From dab0cc72806c0e9b71fc13b436d1ad237cf56e78 Mon Sep 17 00:00:00 2001 From: Adi Date: Mon, 8 Jan 2018 01:23:40 -0500 Subject: [PATCH 23/64] Update module name --- test/config_test.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/config_test.exs b/test/config_test.exs index 2fdb52a..d198a4e 100644 --- a/test/config_test.exs +++ b/test/config_test.exs @@ -1,4 +1,4 @@ -defmodule Rummage.Ecto.ConfiTest do +defmodule Rummage.Ecto.ConfigTest do use ExUnit.Case doctest Rummage.Ecto.Config end From bba6f2d5a46191ef52e8fa385418117c0384b7c5 Mon Sep 17 00:00:00 2001 From: Adi Date: Mon, 8 Jan 2018 01:23:56 -0500 Subject: [PATCH 24/64] Update docs/defaults for custom hooks: WIP- work with new hook structure --- lib/rummage_ecto/custom_hooks/keyset_paginate.ex | 6 +++--- lib/rummage_ecto/custom_hooks/simple_search.ex | 2 +- lib/rummage_ecto/custom_hooks/simple_sort.ex | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/rummage_ecto/custom_hooks/keyset_paginate.ex b/lib/rummage_ecto/custom_hooks/keyset_paginate.ex index d7b88c3..6a50780 100644 --- a/lib/rummage_ecto/custom_hooks/keyset_paginate.ex +++ b/lib/rummage_ecto/custom_hooks/keyset_paginate.ex @@ -17,7 +17,7 @@ defmodule Rummage.Ecto.CustomHooks.KeysetPaginate do ```elixir config :rummage_ecto, Rummage.Ecto, - default_paginate: Rummage.Ecto.CustomHooks.KeysetPaginate + _paginate: Rummage.Ecto.CustomHooks.KeysetPaginate ``` """ @@ -166,12 +166,12 @@ defmodule Rummage.Ecto.CustomHooks.KeysetPaginate do defp get_total_count(queryable, opts), do: length(apply(get_repo(opts), :all, [queryable])) defp get_repo(opts) do - opts[:repo] || Config.default_repo + opts[:repo] || Config.repo end defp parse_page_and_per_page(paginate_params, opts) do per_page = paginate_params - |> Map.get("per_page", Integer.to_string(opts[:per_page] || Config.default_per_page)) + |> Map.get("per_page", Integer.to_string(opts[:per_page] || Config.per_page)) |> String.to_integer page = paginate_params diff --git a/lib/rummage_ecto/custom_hooks/simple_search.ex b/lib/rummage_ecto/custom_hooks/simple_search.ex index 9b89376..973ff2b 100644 --- a/lib/rummage_ecto/custom_hooks/simple_search.ex +++ b/lib/rummage_ecto/custom_hooks/simple_search.ex @@ -42,7 +42,7 @@ defmodule Rummage.Ecto.CustomHooks.SimpleSearch do ```elixir config :rummage_ecto, Rummage.Ecto, - default_search: Rummage.Ecto.CustomHooks.SimpleSearch + .search: Rummage.Ecto.CustomHooks.SimpleSearch ``` """ diff --git a/lib/rummage_ecto/custom_hooks/simple_sort.ex b/lib/rummage_ecto/custom_hooks/simple_sort.ex index 6c94480..4309056 100644 --- a/lib/rummage_ecto/custom_hooks/simple_sort.ex +++ b/lib/rummage_ecto/custom_hooks/simple_sort.ex @@ -39,7 +39,7 @@ defmodule Rummage.Ecto.CustomHooks.SimpleSort do ```elixir config :rummage_ecto, Rummage.Ecto, - default_sort: Rummage.Ecto.CustomHooks.SimpleSort + .sort: Rummage.Ecto.CustomHooks.SimpleSort ``` """ From c66b23b8d1ef1e55787d9772e03ccee2fe1fe1a5 Mon Sep 17 00:00:00 2001 From: Adi Date: Mon, 8 Jan 2018 01:24:29 -0500 Subject: [PATCH 25/64] Update with new docs --- .../services/build_search_query.ex | 25 +++++++------------ 1 file changed, 9 insertions(+), 16 deletions(-) diff --git a/lib/rummage_ecto/services/build_search_query.ex b/lib/rummage_ecto/services/build_search_query.ex index 14ca40f..3c7c552 100644 --- a/lib/rummage_ecto/services/build_search_query.ex +++ b/lib/rummage_ecto/services/build_search_query.ex @@ -6,10 +6,10 @@ defmodule Rummage.Ecto.Services.BuildSearchQuery do Has a `Module Attribute` called `search_types`: ```elixir - @search_types ~w(like ilike eq gt lt gteq lteq) + @search_types ~w(like ilike eq gt lt gteq lteq is_null) ``` - `@search_types` is a collection of all the 7 valid `search_types` that come shipped with + `@search_types` is a collection of all the 8 valid `search_types` that come shipped with `Rummage.Ecto`'s default search hook. The types are: * `like`: Searches for a `term` in a given `field` of a `queryable`. @@ -44,12 +44,12 @@ defmodule Rummage.Ecto.Services.BuildSearchQuery do When `field`, `search_type` and `queryable` are passed with `search_type` of `ilike`: - iex> alias Rummage.Ecto.Services.BuildSearchQuery - iex> import Ecto.Query - iex> queryable = from u in "parents" - #Ecto.Query - iex> BuildSearchQuery.run(queryable, :field_1, "ilike", "field_!") - #Ecto.Query + iex> alias Rummage.Ecto.Services.BuildSearchQuery + iex> import Ecto.Query + iex> queryable = from u in "parents" + #Ecto.Query + iex> BuildSearchQuery.run(queryable, :field_1, "ilike", "field_!") + #Ecto.Query When `field`, `search_type` and `queryable` are passed with `search_type` of `eq`: @@ -104,6 +104,7 @@ When `field`, `search_type` and `queryable` are passed with an invalid `search_t #Ecto.Query iex> BuildSearchQuery.run(queryable, :field_1, "pizza", "field_!") ** (RuntimeError) Unknown search_type pizza + """ @spec run(Ecto.Query.t, atom, String.t, term) :: {Ecto.Query.t} def run(queryable, field, search_type, search_term) do @@ -133,7 +134,6 @@ When `field`, `search_type` and `queryable` are passed with an invalid `search_t like(field(b, ^field), ^"%#{String.replace(search_term, "%", "\\%")}%")) end - @doc """ Builds a searched `queryable` on top of the given `queryable` using `field` and `search_type` when the `search_term` is `ilike`. @@ -154,7 +154,6 @@ When `field`, `search_type` and `queryable` are passed with an invalid `search_t ilike(field(b, ^field), ^"%#{String.replace(search_term, "%", "\\%")}%")) end - @doc """ Builds a searched `queryable` on top of the given `queryable` using `field` and `search_type` when the `search_term` is `eq`. @@ -175,7 +174,6 @@ When `field`, `search_type` and `queryable` are passed with an invalid `search_t field(b, ^field) == ^search_term) end - @doc """ Builds a searched `queryable` on top of the given `queryable` using `field` and `search_type` when the `search_term` is `gt`. @@ -196,7 +194,6 @@ When `field`, `search_type` and `queryable` are passed with an invalid `search_t field(b, ^field) > ^search_term) end - @doc """ Builds a searched `queryable` on top of the given `queryable` using `field` and `search_type` when the `search_term` is `lt`. @@ -217,7 +214,6 @@ When `field`, `search_type` and `queryable` are passed with an invalid `search_t field(b, ^field) < ^search_term) end - @doc """ Builds a searched `queryable` on top of the given `queryable` using `field` and `search_type` when the `search_term` is `gteq`. @@ -238,7 +234,6 @@ When `field`, `search_type` and `queryable` are passed with an invalid `search_t field(b, ^field) >= ^search_term) end - @doc """ Builds a searched `queryable` on top of the given `queryable` using `field` and `search_type` when the `search_term` is `lteq`. @@ -276,13 +271,11 @@ When `field`, `search_type` and `queryable` are passed with an invalid `search_t @spec handle_is_null(Ecto.Query.t, atom, term) :: {Ecto.Query.t} def handle_is_null(queryable, field, term \\ true) - def handle_is_null(queryable, field, term) when term in ["true", true, 1] do queryable |> where([..., b], is_nil(field(b, ^field))) end - def handle_is_null(queryable, field, term) when term in ["false", false, 0] do queryable |> where([..., b], From 07f7c31100a23a25c0d7504798e35e946524c38b Mon Sep 17 00:00:00 2001 From: Adi Date: Mon, 8 Jan 2018 01:24:46 -0500 Subject: [PATCH 26/64] [WIP] Update rummage ecto (more functional) --- lib/rummage_ecto.ex | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/lib/rummage_ecto.ex b/lib/rummage_ecto.ex index 900b668..d888c67 100644 --- a/lib/rummage_ecto.ex +++ b/lib/rummage_ecto.ex @@ -1,5 +1,5 @@ defmodule Rummage.Ecto do - @moduledoc ~S""" + @moduledoc """ Rummage.Ecto is a light weight, but powerful framework that can be used to alter Ecto queries with Search, Sort and Paginate operations. @@ -34,12 +34,14 @@ defmodule Rummage.Ecto do alias Rummage.Ecto.Config + @hooks [search: :default, sort: :default, paginate: :default] + @doc """ This is the function which calls to the `Rummage` `hooks`. It is the entry-point to `Rummage.Ecto`. This function takes in a `queryable`, a `rummage` struct and an `opts` map. Possible `opts` values are: - - `repo`: If you haven't set up a `default_repo`, or are using an app that uses multiple repos, this might come handy. - This overrides the `default_repo` in the configuration. + - `repo`: If you haven't set up a .repo`, or are using an app that uses multiple repos, this might come handy. + This overrides the .repo` in the configuration. - `hooks`: This allows us to specify what `Rummage` hooks to use in this `rummage` lifecycle. It defaults to `[:search, :sort, :paginate]`. This also allows us to specify the order of `hooks` operation, if in case they @@ -53,7 +55,7 @@ defmodule Rummage.Ecto do the default values for repo and per_page: iex> rummage = %{"search" => %{}, "sort" => %{}, "paginate" => %{}} - iex> {queryable, rummage} = Rummage.Ecto.rummage(Rummage.Ecto.Product, rummage) # We have set a default_repo in the configuration to Rummage.Ecto.Repo + iex> {queryable, rummage} = Rummage.Ecto.rummage(Rummage.Ecto.Product, rummage) # We have set a.repo in the configuration to Rummage.Ecto.Repo iex> rummage %{"paginate" => %{"max_page" => "0", "page" => "1", "per_page" => "2", "total_count" => "0"}, "search" => %{}, @@ -97,24 +99,22 @@ defmodule Rummage.Ecto do """ - @default_hooks [search: :default, sort: :default, paginate: :default] - @spec rummage(Ecto.Query.t, map, map) :: {Ecto.Query.t, map} def rummage(queryable, rummage, opts \\ []) def rummage(queryable, rummage, _opts) when rummage == nil, do: {queryable, %{}} def rummage(queryable, rummage, opts) do - hooks = opts[:hooks] || @default_hooks + hooks = [opts[:search] || Rummage.Ecto.Config.search(), + opts[:sort] || Rummage.Ecto.Config.sort(), + opts[:paginate] || Rummage.Ecto.Config.paginate()] Enum.reduce(hooks, {queryable, rummage}, &apply_mod(&1, &2, opts)) end - defp apply_mod({type, mod}, {queryable, rummage}, opts) do - mod = mod == :default && apply(Config, type, []) || mod - + defp apply_mod(mod, {queryable, rummage}, opts) do {apply(mod, :run, [queryable, rummage]), apply(mod, :before_hook, [queryable, rummage, opts])} end - defmacro __using__(_opts) do + defmacro __using__(opts) do quote do require Rummage.Ecto From 57d3859d1e0c9a67600678b266291f277b195316 Mon Sep 17 00:00:00 2001 From: Adi Date: Mon, 8 Jan 2018 01:46:11 -0500 Subject: [PATCH 27/64] Update docs for Search hook --- lib/rummage_ecto/hooks/search.ex | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/rummage_ecto/hooks/search.ex b/lib/rummage_ecto/hooks/search.ex index 52a87ab..142c200 100644 --- a/lib/rummage_ecto/hooks/search.ex +++ b/lib/rummage_ecto/hooks/search.ex @@ -1,5 +1,7 @@ defmodule Rummage.Ecto.Hooks.Search do @moduledoc """ + TODO: Explain how to use `assoc` better + `Rummage.Ecto.Hooks.Search` is the default search hook that comes with `Rummage.Ecto`. @@ -7,9 +9,6 @@ defmodule Rummage.Ecto.Hooks.Search do a pipeline of `Ecto` queries. This module works by taking fields, and `search_type`, `search_term` and `assoc` associated with those `fields`. - TODO: Explain how to use `assoc` better - - NOTE: This module doesn't return a list of entries, but a `Ecto.Query.t`. From 6514502b5308437aa6cfeb378bbd2f322aeb1348 Mon Sep 17 00:00:00 2001 From: Adi Date: Mon, 8 Jan 2018 17:46:01 -0500 Subject: [PATCH 28/64] Change configuration strategy: - Add appname support - This allows rummage to be configured for multiple apps in an umbrella --- lib/rummage_ecto/config.ex | 55 +++++++++++++++++++++++++++----------- 1 file changed, 40 insertions(+), 15 deletions(-) diff --git a/lib/rummage_ecto/config.ex b/lib/rummage_ecto/config.ex index 1adc7ca..d01c4f6 100644 --- a/lib/rummage_ecto/config.ex +++ b/lib/rummage_ecto/config.ex @@ -2,11 +2,36 @@ defmodule Rummage.Ecto.Config do @moduledoc """ This module encapsulates all the Rummage's runtime configurations that can be set in the config.exs file. + + __This configuration is optional, as `Rummage.Ecto` can accept the same + arguments as optional arguments to the function `Rummage.Ecto.rummage/3`__ + + ## Usage: + + A basic example without overriding default hooks: + ### config.exs: + + config :app_name, Rummage.Ecto, + per_page: 10, + repo: AppName.Repo + + This is a more advanced usage where you can specify the default hooks: + ### config.exs: + + config :app_name, Rummage.Ecto, + per_page: 10, + repo: AppName.Repo, + search: Rummage.Ecto.Hooks.Search, + sort: Rummage.Ecto.Hooks.Sort, + paginate: Rummage.Ecto.Hooks.Paginate + """ @doc """ `:search` hook can also be set at run time - in the `config.exs` file + in the `config.exs` file. This pulls the configuration + assocated with the application, `application`. When no + application is given this defaults to `rummage_ecto`. ## Examples When no config is set, if returns the default hook @@ -15,8 +40,8 @@ defmodule Rummage.Ecto.Config do iex> Config.search Rummage.Ecto.Hooks.Search """ - def search do - config(:search, Rummage.Ecto.Hooks.Search) + def search(application \\ :rummage_ecto) do + config(:search, Rummage.Ecto.Hooks.Search, application) end @doc """ @@ -30,8 +55,8 @@ defmodule Rummage.Ecto.Config do iex> Config.sort Rummage.Ecto.Hooks.Sort """ - def sort do - config(:sort, Rummage.Ecto.Hooks.Sort) + def sort(application \\ :rummage_ecto) do + config(:sort, Rummage.Ecto.Hooks.Sort, application) end @doc """ @@ -45,8 +70,8 @@ defmodule Rummage.Ecto.Config do iex> Config.paginate Rummage.Ecto.Hooks.Paginate """ - def paginate do - config(:paginate, Rummage.Ecto.Hooks.Paginate) + def paginate(application \\ :rummage_ecto) do + config(:paginate, Rummage.Ecto.Hooks.Paginate, application) end @doc """ @@ -60,8 +85,8 @@ defmodule Rummage.Ecto.Config do iex> Config.per_page 2 """ - def per_page do - config(:per_page, 10) + def per_page(application \\ :rummage_ecto) do + config(:per_page, 10, application) end @doc """ @@ -75,16 +100,16 @@ defmodule Rummage.Ecto.Config do iex> Config.repo Rummage.Ecto.Repo """ - def repo do - config(:repo, nil) + def repo(application \\ :rummage_ecto) do + config(:repo, nil, application) end - defp config do - Application.get_env(:rummage_ecto, Rummage.Ecto, []) + defp config(application) do + Application.get_env(application, Rummage.Ecto, []) end - defp config(key, default) do - config() + defp config(key, default, application) do + config(application) |> Keyword.get(key, default) |> resolve_config(default) end From a7c49dbd80518e434eb0fe1645ac4e8f443cae4e Mon Sep 17 00:00:00 2001 From: Adi Date: Mon, 8 Jan 2018 17:49:22 -0500 Subject: [PATCH 29/64] Update test config --- config/test.exs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/config/test.exs b/config/test.exs index 00ff3a7..7e28c6b 100644 --- a/config/test.exs +++ b/config/test.exs @@ -3,10 +3,9 @@ use Mix.Config config :logger, :console, level: :error -config :rummage_ecto, Rummage.Ecto,[ +config :rummage_ecto, Rummage.Ecto, repo: Rummage.Ecto.Repo, - per_page: 2, -] + per_page: 2 config :rummage_ecto, ecto_repos: [Rummage.Ecto.Repo] From db7599756771656d8a8b93778d8df9b13bab3dc9 Mon Sep 17 00:00:00 2001 From: Adi Date: Mon, 8 Jan 2018 17:52:12 -0500 Subject: [PATCH 30/64] Update comments and documentation: - Improve comments on search module --- lib/rummage_ecto/hooks/search.ex | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/rummage_ecto/hooks/search.ex b/lib/rummage_ecto/hooks/search.ex index 142c200..3e4be8e 100644 --- a/lib/rummage_ecto/hooks/search.ex +++ b/lib/rummage_ecto/hooks/search.ex @@ -160,12 +160,17 @@ defmodule Rummage.Ecto.Hooks.Search do @spec run(Ecto.Query.t(), map()) :: Ecto.Query.t() def run(q, s), do: handle_search(q, s) + # Helper function which handles addition of search query on top of + # the sent queryable variable, for all search fields. defp handle_search(queryable, search_params) do search_params |> Map.to_list() |> Enum.reduce(queryable, &search_queryable(&1, &2)) end + # Helper function which handles addition of search query on top of + # the sent queryable variable, for ONE search fields. + # This delegates the query building to `BuildSearchQuery` module defp search_queryable(param, queryable) do field = elem(param, 0) field_params = elem(param, 1) @@ -181,7 +186,8 @@ defmodule Rummage.Ecto.Hooks.Search do |> BuildSearchQuery.run(field, search_type, search_term) end - # TODO: Support other join types + # Helper function which handles associations in a query with a join + # type. defp join_by_assoc({join, assoc}, query) do join(query, join, [..., p1], p2 in assoc(p1, ^String.to_atom(assoc))) end From 0bf8f42091f90460887e726b66af65bd6b93781b Mon Sep 17 00:00:00 2001 From: Adi Date: Mon, 8 Jan 2018 17:52:37 -0500 Subject: [PATCH 31/64] Update rummage_ecto.exs to work with Ecto.Schema: - Ecto Schemas can now `use` Rummage.Ecto. - `__using__` Rummage.Ecto now brings in defaults from the app that the user nodule belongs. - [WIP] Work on documentation --- lib/rummage_ecto.ex | 35 ++++++++++++++++++++++++++++------- 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/lib/rummage_ecto.ex b/lib/rummage_ecto.ex index d888c67..49be498 100644 --- a/lib/rummage_ecto.ex +++ b/lib/rummage_ecto.ex @@ -43,13 +43,13 @@ defmodule Rummage.Ecto do - `repo`: If you haven't set up a .repo`, or are using an app that uses multiple repos, this might come handy. This overrides the .repo` in the configuration. - - `hooks`: This allows us to specify what `Rummage` hooks to use in this `rummage` lifecycle. It defaults to - `[:search, :sort, :paginate]`. This also allows us to specify the order of `hooks` operation, if in case they - need to be changed. - - `search`: This allows us to override a `Rummage.Ecto.Hook` with a `CustomHook`. This `CustomHook` must implement the behavior `Rummage.Ecto.Hook`. + - `sort`: + + - `paginate`: + ## Examples When no `repo` or `per_page` key is given in the `opts` map, it uses the default values for repo and per_page: @@ -114,11 +114,32 @@ defmodule Rummage.Ecto do apply(mod, :before_hook, [queryable, rummage, opts])} end + @doc """ + TODO: Add some crazy docs! + """ defmacro __using__(opts) do quote do - require Rummage.Ecto - - defdelegate rummage(queryable, rummage, opts \\ []), to: Rummage.Ecto + alias Rummage.Ecto.Config, as: RConfig + + def rummage(queryable, rummage, opts \\ []) do + Rummage.Ecto.rummage(queryable, rummage, uniq_merge(opts, defaults())) + end + + defp defaults() do + keys = ~w(repo per_page search sort paginate)a + Enum.map(keys, &get_defs/1) + end + + defp get_defs(key) do + app = Application.get_application(__MODULE__) + {key, Keyword.get(unquote(opts), key, apply(RConfig, key, [app]))} + end + + defp uniq_merge(keyword1, keyword2) do + keyword2 + |> Keyword.merge(keyword1) + |> Keyword.new() + end end end end From 06e45577e8beb8fa07fa9ddc036240fcc362b7ee Mon Sep 17 00:00:00 2001 From: Adi Date: Mon, 8 Jan 2018 17:53:51 -0500 Subject: [PATCH 32/64] Update sort_hook credo changes --- lib/rummage_ecto/hooks/sort.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/rummage_ecto/hooks/sort.ex b/lib/rummage_ecto/hooks/sort.ex index e0cb6c3..1681a64 100644 --- a/lib/rummage_ecto/hooks/sort.ex +++ b/lib/rummage_ecto/hooks/sort.ex @@ -196,7 +196,7 @@ defmodule Rummage.Ecto.Hooks.Sort do sort_params = {sort_params["assoc"], order_param} - handle_sort(queryable,sort_params, true) + handle_sort(queryable, sort_params, true) _ -> handle_sort(queryable, {sort_params["assoc"], sort_params["field"]}) end From fc7c539d82aceadfaa39ef75504b29159151cd3c Mon Sep 17 00:00:00 2001 From: Adi Date: Mon, 8 Jan 2018 19:19:22 -0500 Subject: [PATCH 33/64] Update BuildSearchQuery: - Update docs - Add more tests - Add HM types - Clean up and improve test coverage - Add support for search_exprs - Add `or_where` search_expr - Add `not_where` search_expr - Add tests and documentation for the above changes --- .../services/build_search_query.ex | 596 +++++++++++++++--- 1 file changed, 499 insertions(+), 97 deletions(-) diff --git a/lib/rummage_ecto/services/build_search_query.ex b/lib/rummage_ecto/services/build_search_query.ex index 3c7c552..1ab0b2d 100644 --- a/lib/rummage_ecto/services/build_search_query.ex +++ b/lib/rummage_ecto/services/build_search_query.ex @@ -6,7 +6,8 @@ defmodule Rummage.Ecto.Services.BuildSearchQuery do Has a `Module Attribute` called `search_types`: ```elixir - @search_types ~w(like ilike eq gt lt gteq lteq is_null) + @search_types ~w(like ilike eq gt lt gteq lteq is_null)a + @search_exprs ~w(where or_where not_where)a ``` `@search_types` is a collection of all the 8 valid `search_types` that come shipped with @@ -21,264 +22,665 @@ defmodule Rummage.Ecto.Services.BuildSearchQuery do * `lteq`: Searches for a `term` to be less than or equal to a given `field` of a `queryable`. * `is_null`: Searches for a null value when `term` is true, or not null when `term` is false. + `@search_exprs` is a collection of 3 valid `search_exprs` that are used to + apply a `search_type` to a `Ecto.Queryable`. Those expressions are: + + * `where` (DEFAULT): An AND where query expression. + * `or_where`: An OR where query expression. Behaves exactly like where but + combines the previous expression with an `OR` operation. Useful + for optional searches. + * `not_where`: A NOT where query expression. This can be used while excluding + a list of entries based on where query. + Feel free to use this module on a custom search hook that you write. """ import Ecto.Query - @search_types ~w(like ilike eq gt lt gteq lteq is_null) + @typedoc ~s(TODO: Finish) + @type search_expr :: :where | :or_where | :not_where + + @typedoc ~s(TODO: Finish) + @type search_type :: :like | :ilike | :eq | :gt + | :lt | :gteq | :lteq | :is_null + + @search_types ~w(like ilike eq gt lt gteq lteq is_null)a + @search_exprs ~w(where or_where not_where)a @doc """ Builds a searched `queryable` on top of the given `queryable` using `field`, `search_type` and `search_term`. ## Examples - When `field`, `search_type` and `queryable` are passed with `search_type` of `like`: + When `search_type` is `where`: + When `field`, `search_type` and `queryable` are passed with `search_type` of `like`: + + iex> alias Rummage.Ecto.Services.BuildSearchQuery + iex> import Ecto.Query + iex> queryable = from u in "parents" + #Ecto.Query + iex> BuildSearchQuery.run(queryable, :field_1, {:where, :like}, "field_!") + #Ecto.Query + + When `field`, `search_type` and `queryable` are passed with `search_type` of `ilike`: + + iex> alias Rummage.Ecto.Services.BuildSearchQuery + iex> import Ecto.Query + iex> queryable = from u in "parents" + #Ecto.Query + iex> BuildSearchQuery.run(queryable, :field_1, {:where, :ilike}, "field_!") + #Ecto.Query + + When `field`, `search_type` and `queryable` are passed with `search_type` of `eq`: + + iex> alias Rummage.Ecto.Services.BuildSearchQuery + iex> import Ecto.Query + iex> queryable = from u in "parents" + #Ecto.Query + iex> BuildSearchQuery.run(queryable, :field_1, {:where, :eq}, "field_!") + #Ecto.Query + + When `field`, `search_type` and `queryable` are passed with `search_type` of `gt`: + + iex> alias Rummage.Ecto.Services.BuildSearchQuery + iex> import Ecto.Query + iex> queryable = from u in "parents" + #Ecto.Query + iex> BuildSearchQuery.run(queryable, :field_1, {:where, :gt}, "field_!") + #Ecto.Query ^"field_!"> + + When `field`, `search_type` and `queryable` are passed with `search_type` of `lt`: + + iex> alias Rummage.Ecto.Services.BuildSearchQuery + iex> import Ecto.Query + iex> queryable = from u in "parents" + #Ecto.Query + iex> BuildSearchQuery.run(queryable, :field_1, {:where, :lt}, "field_!") + #Ecto.Query + + When `field`, `search_type` and `queryable` are passed with `search_type` of `gteq`: + + iex> alias Rummage.Ecto.Services.BuildSearchQuery + iex> import Ecto.Query + iex> queryable = from u in "parents" + #Ecto.Query + iex> BuildSearchQuery.run(queryable, :field_1, {:where, :gteq}, "field_!") + #Ecto.Query= ^"field_!"> + + When `field`, `search_type` and `queryable` are passed with `search_type` of `lteq`: + + iex> alias Rummage.Ecto.Services.BuildSearchQuery + iex> import Ecto.Query + iex> queryable = from u in "parents" + #Ecto.Query + iex> BuildSearchQuery.run(queryable, :field_1, {:where, :lteq}, "field_!") + #Ecto.Query + + When `field`, `search_type` and `queryable` are passed with an invalid `search_type` + and `search_expr`: iex> alias Rummage.Ecto.Services.BuildSearchQuery iex> import Ecto.Query iex> queryable = from u in "parents" #Ecto.Query - iex> BuildSearchQuery.run(queryable, :field_1, "like", "field_!") - #Ecto.Query + iex> BuildSearchQuery.run(queryable, :field_1, {:pizza, :cheese}, "field_!") + ** (RuntimeError) Unknown {search_expr, search_type}, {:pizza, :cheese} + search_type should be one of #{inspect @search_types} + search_expr should be one of #{inspect @search_exprs} + - When `field`, `search_type` and `queryable` are passed with `search_type` of `ilike`: + When `field`, `search_type` and `queryable` are passed with an invalid `search_type` + but valid `search_expr: iex> alias Rummage.Ecto.Services.BuildSearchQuery iex> import Ecto.Query iex> queryable = from u in "parents" #Ecto.Query - iex> BuildSearchQuery.run(queryable, :field_1, "ilike", "field_!") - #Ecto.Query + iex> BuildSearchQuery.run(queryable, :field_1, {:where, :pizza}, "field_!") + ** (RuntimeError) Unknown {search_expr, search_type}, {:where, :pizza} + search_type should be one of #{inspect @search_types} + search_expr should be one of #{inspect @search_exprs} + + When `search_type` is `or_where`: + + When `field`, `search_type` and `queryable` are passed with `search_type` of `like`: + + iex> alias Rummage.Ecto.Services.BuildSearchQuery + iex> import Ecto.Query + iex> queryable = from u in "parents" + #Ecto.Query + iex> BuildSearchQuery.run(queryable, :field_1, {:or_where, :like}, "field_!") + #Ecto.Query + + When `field`, `search_type` and `queryable` are passed with `search_type` of `ilike`: + + iex> alias Rummage.Ecto.Services.BuildSearchQuery + iex> import Ecto.Query + iex> queryable = from u in "parents" + #Ecto.Query + iex> BuildSearchQuery.run(queryable, :field_1, {:or_where, :ilike}, "field_!") + #Ecto.Query + + When `field`, `search_type` and `queryable` are passed with `search_type` of `eq`: + + iex> alias Rummage.Ecto.Services.BuildSearchQuery + iex> import Ecto.Query + iex> queryable = from u in "parents" + #Ecto.Query + iex> BuildSearchQuery.run(queryable, :field_1, {:or_where, :eq}, "field_!") + #Ecto.Query + + When `field`, `search_type` and `queryable` are passed with `search_type` of `gt`: + + iex> alias Rummage.Ecto.Services.BuildSearchQuery + iex> import Ecto.Query + iex> queryable = from u in "parents" + #Ecto.Query + iex> BuildSearchQuery.run(queryable, :field_1, {:or_where, :gt}, "field_!") + #Ecto.Query ^"field_!"> + + When `field`, `search_type` and `queryable` are passed with `search_type` of `lt`: + + iex> alias Rummage.Ecto.Services.BuildSearchQuery + iex> import Ecto.Query + iex> queryable = from u in "parents" + #Ecto.Query + iex> BuildSearchQuery.run(queryable, :field_1, {:or_where, :lt}, "field_!") + #Ecto.Query + + When `field`, `search_type` and `queryable` are passed with `search_type` of `gteq`: + + iex> alias Rummage.Ecto.Services.BuildSearchQuery + iex> import Ecto.Query + iex> queryable = from u in "parents" + #Ecto.Query + iex> BuildSearchQuery.run(queryable, :field_1, {:or_where, :gteq}, "field_!") + #Ecto.Query= ^"field_!"> + + When `field`, `search_type` and `queryable` are passed with `search_type` of `lteq`: + + iex> alias Rummage.Ecto.Services.BuildSearchQuery + iex> import Ecto.Query + iex> queryable = from u in "parents" + #Ecto.Query + iex> BuildSearchQuery.run(queryable, :field_1, {:or_where, :lteq}, "field_!") + #Ecto.Query + """ + @spec run(Ecto.Query.t, {__MODULE__.search_expr(), __MODULE__.search_type()}, + String.t, term) :: {Ecto.Query.t} + def run(queryable, field, {search_expr, search_type}, search_term) + when search_type in @search_types and search_expr in @search_exprs + do + apply(__MODULE__, String.to_atom("handle_" <> to_string(search_type)), + [queryable, field, search_term, search_expr]) + end + def run(_, _, search_tuple, _) do + raise "Unknown {search_expr, search_type}, #{inspect search_tuple}\n" <> + "search_type should be one of #{inspect @search_types}\n" <> + "search_expr should be one of #{inspect @search_exprs}" + end + + @doc """ + Builds a searched `queryable` on top of the given `queryable` using + `field`, `search_term` and `search_expr` when the `search_type` is `like`. - When `field`, `search_type` and `queryable` are passed with `search_type` of `eq`: + Checkout [Ecto.Query.API.like/2](https://hexdocs.pm/ecto/Ecto.Query.API.html#like/2) + for more info. + + NOTE: Be careful of [Like Injections](https://githubengineering.com/like-injection/) + + Assumes that `search_expr` is in #{inspect @search_exprs}. + + ## Examples + + When `search_expr` is `:where` iex> alias Rummage.Ecto.Services.BuildSearchQuery iex> import Ecto.Query iex> queryable = from u in "parents" #Ecto.Query - iex> BuildSearchQuery.run(queryable, :field_1, "eq", "field_!") - #Ecto.Query + iex> BuildSearchQuery.handle_like(queryable, :field_1, "field_!", :where) + #Ecto.Query - When `field`, `search_type` and `queryable` are passed with `search_type` of `gt`: + When `search_expr` is `:or_where` iex> alias Rummage.Ecto.Services.BuildSearchQuery iex> import Ecto.Query iex> queryable = from u in "parents" #Ecto.Query - iex> BuildSearchQuery.run(queryable, :field_1, "gt", "field_!") - #Ecto.Query ^"field_!"> + iex> BuildSearchQuery.handle_like(queryable, :field_1, "field_!", :or_where) + #Ecto.Query - When `field`, `search_type` and `queryable` are passed with `search_type` of `lt`: + When `search_expr` is `:not_where` iex> alias Rummage.Ecto.Services.BuildSearchQuery iex> import Ecto.Query iex> queryable = from u in "parents" #Ecto.Query - iex> BuildSearchQuery.run(queryable, :field_1, "lt", "field_!") - #Ecto.Query + iex> BuildSearchQuery.handle_like(queryable, :field_1, "field_!", :not_where) + #Ecto.Query + + """ + @spec handle_like(Ecto.Query.t(), atom(), String.t(), + __MODULE__.search_expr()) :: Ecto.Query.t() + def handle_like(queryable, field, search_term, :where) do + queryable + |> where([..., b], + like(field(b, ^field), ^"%#{String.replace(search_term, "%", "\\%")}%")) + end + def handle_like(queryable, field, search_term, :or_where) do + queryable + |> or_where([..., b], + like(field(b, ^field), ^"%#{String.replace(search_term, "%", "\\%")}%")) + end + def handle_like(queryable, field, search_term, :not_where) do + queryable + |> where([..., b], + not like(field(b, ^field), ^"%#{String.replace(search_term, "%", "\\%")}%")) + end + + @doc """ + Builds a searched `queryable` on top of the given `queryable` using + `field`, `search_term` and `search_expr` when the `search_type` is `ilike`. + + Checkout [Ecto.Query.API.ilike/2](https://hexdocs.pm/ecto/Ecto.Query.API.html#ilike/2) + for more info. + + Assumes that `search_expr` is in #{inspect @search_exprs}. + + ## Examples - When `field`, `search_type` and `queryable` are passed with `search_type` of `gteq`: + When `search_expr` is `:where` iex> alias Rummage.Ecto.Services.BuildSearchQuery iex> import Ecto.Query iex> queryable = from u in "parents" #Ecto.Query - iex> BuildSearchQuery.run(queryable, :field_1, "gteq", "field_!") - #Ecto.Query= ^"field_!"> + iex> BuildSearchQuery.handle_ilike(queryable, :field_1, "field_!", :where) + #Ecto.Query - When `field`, `search_type` and `queryable` are passed with `search_type` of `lteq`: + When `search_expr` is `:or_where` iex> alias Rummage.Ecto.Services.BuildSearchQuery iex> import Ecto.Query iex> queryable = from u in "parents" #Ecto.Query - iex> BuildSearchQuery.run(queryable, :field_1, "lteq", "field_!") - #Ecto.Query + iex> BuildSearchQuery.handle_ilike(queryable, :field_1, "field_!", :or_where) + #Ecto.Query -When `field`, `search_type` and `queryable` are passed with an invalid `search_type`: + When `search_expr` is `:not_where` iex> alias Rummage.Ecto.Services.BuildSearchQuery iex> import Ecto.Query iex> queryable = from u in "parents" #Ecto.Query - iex> BuildSearchQuery.run(queryable, :field_1, "pizza", "field_!") - ** (RuntimeError) Unknown search_type pizza + iex> BuildSearchQuery.handle_ilike(queryable, :field_1, "field_!", :not_where) + #Ecto.Query """ - @spec run(Ecto.Query.t, atom, String.t, term) :: {Ecto.Query.t} - def run(queryable, field, search_type, search_term) do - case Enum.member?(@search_types, search_type) do - true -> apply(__MODULE__, String.to_atom("handle_" <> search_type), [queryable, field, search_term]) - _ -> raise "Unknown search_type #{search_type}" - end + @spec handle_ilike(Ecto.Query.t(), atom(), String.t(), + __MODULE__.search_expr()) :: Ecto.Query.t() + def handle_ilike(queryable, field, search_term, :where) do + queryable + |> where([..., b], + ilike(field(b, ^field), ^"%#{String.replace(search_term, "%", "\\%")}%")) + end + def handle_ilike(queryable, field, search_term, :or_where) do + queryable + |> or_where([..., b], + ilike(field(b, ^field), ^"%#{String.replace(search_term, "%", "\\%")}%")) + end + def handle_ilike(queryable, field, search_term, :not_where) do + queryable + |> where([..., b], + not ilike(field(b, ^field), ^"%#{String.replace(search_term, "%", "\\%")}%")) end @doc """ - Builds a searched `queryable` on top of the given `queryable` using `field` and `search_type` - when the `search_term` is `like`. + Builds a searched `queryable` on top of the given `queryable` using + `field`, `search_term` and `search_expr` when the `search_type` is `eq`. + + Assumes that `search_expr` is in #{inspect @search_exprs}. ## Examples + When `search_expr` is `:where` + iex> alias Rummage.Ecto.Services.BuildSearchQuery iex> import Ecto.Query iex> queryable = from u in "parents" #Ecto.Query - iex> BuildSearchQuery.handle_like(queryable, :field_1, "field_!") - #Ecto.Query - """ - @spec handle_like(Ecto.Query.t, atom, term) :: {Ecto.Query.t} - def handle_like(queryable, field, search_term) do - queryable - |> where([..., b], - like(field(b, ^field), ^"%#{String.replace(search_term, "%", "\\%")}%")) - end + iex> BuildSearchQuery.handle_eq(queryable, :field_1, "field_!", :where) + #Ecto.Query - @doc """ - Builds a searched `queryable` on top of the given `queryable` using `field` and `search_type` - when the `search_term` is `ilike`. + When `search_expr` is `:or_where` - ## Examples + iex> alias Rummage.Ecto.Services.BuildSearchQuery + iex> import Ecto.Query + iex> queryable = from u in "parents" + #Ecto.Query + iex> BuildSearchQuery.handle_eq(queryable, :field_1, "field_!", :or_where) + #Ecto.Query + + When `search_expr` is `:not_where` iex> alias Rummage.Ecto.Services.BuildSearchQuery iex> import Ecto.Query iex> queryable = from u in "parents" #Ecto.Query - iex> BuildSearchQuery.handle_ilike(queryable, :field_1, "field_!") - #Ecto.Query + iex> BuildSearchQuery.handle_eq(queryable, :field_1, "field_!", :not_where) + #Ecto.Query + """ - @spec handle_ilike(Ecto.Query.t, atom, term) :: {Ecto.Query.t} - def handle_ilike(queryable, field, search_term) do + @spec handle_eq(Ecto.Query.t(), atom(), term(), + __MODULE__.search_expr()) :: Ecto.Query.t() + def handle_eq(queryable, field, search_term, :where) do queryable |> where([..., b], - ilike(field(b, ^field), ^"%#{String.replace(search_term, "%", "\\%")}%")) + field(b, ^field) == ^search_term) + end + def handle_eq(queryable, field, search_term, :or_where) do + queryable + |> or_where([..., b], + field(b, ^field) == ^search_term) + end + def handle_eq(queryable, field, search_term, :not_where) do + queryable + |> where([..., b], + field(b, ^field) != ^search_term) end @doc """ - Builds a searched `queryable` on top of the given `queryable` using `field` and `search_type` - when the `search_term` is `eq`. + Builds a searched `queryable` on top of the given `queryable` using + `field`, `search_term` and `search_expr` when the `search_type` is `gt`. + + Assumes that `search_expr` is in #{inspect @search_exprs}. ## Examples + When `search_expr` is `:where` + iex> alias Rummage.Ecto.Services.BuildSearchQuery iex> import Ecto.Query iex> queryable = from u in "parents" #Ecto.Query - iex> BuildSearchQuery.handle_eq(queryable, :field_1, "field_!") - #Ecto.Query - """ - @spec handle_eq(Ecto.Query.t, atom, term) :: {Ecto.Query.t} - def handle_eq(queryable, field, search_term) do - queryable - |> where([..., b], - field(b, ^field) == ^search_term) - end + iex> BuildSearchQuery.handle_gt(queryable, :field_1, "field_!", :where) + #Ecto.Query ^\"field_!\"> - @doc """ - Builds a searched `queryable` on top of the given `queryable` using `field` and `search_type` - when the `search_term` is `gt`. + When `search_expr` is `:or_where` - ## Examples + iex> alias Rummage.Ecto.Services.BuildSearchQuery + iex> import Ecto.Query + iex> queryable = from u in "parents" + #Ecto.Query + iex> BuildSearchQuery.handle_gt(queryable, :field_1, "field_!", :or_where) + #Ecto.Query ^\"field_!\"> + + When `search_expr` is `:not_where` iex> alias Rummage.Ecto.Services.BuildSearchQuery iex> import Ecto.Query iex> queryable = from u in "parents" #Ecto.Query - iex> BuildSearchQuery.handle_gt(queryable, :field_1, "field_!") - #Ecto.Query ^"field_!"> + iex> BuildSearchQuery.handle_gt(queryable, :field_1, "field_!", :not_where) + #Ecto.Query + """ - @spec handle_gt(Ecto.Query.t, atom, term) :: {Ecto.Query.t} - def handle_gt(queryable, field, search_term) do + @spec handle_gt(Ecto.Query.t(), atom(), term(), + __MODULE__.search_expr()) :: Ecto.Query.t() + def handle_gt(queryable, field, search_term, :where) do queryable |> where([..., b], field(b, ^field) > ^search_term) end + def handle_gt(queryable, field, search_term, :or_where) do + queryable + |> or_where([..., b], + field(b, ^field) > ^search_term) + end + def handle_gt(queryable, field, search_term, :not_where) do + queryable + |> where([..., b], + field(b, ^field) <= ^search_term) + end @doc """ - Builds a searched `queryable` on top of the given `queryable` using `field` and `search_type` - when the `search_term` is `lt`. + Builds a searched `queryable` on top of the given `queryable` using + `field`, `search_term` and `search_expr` when the `search_type` is `lt`. + + Assumes that `search_expr` is in #{inspect @search_exprs}. ## Examples + When `search_expr` is `:where` + + iex> alias Rummage.Ecto.Services.BuildSearchQuery + iex> import Ecto.Query + iex> queryable = from u in "parents" + #Ecto.Query + iex> BuildSearchQuery.handle_lt(queryable, :field_1, "field_!", :where) + #Ecto.Query + + When `search_expr` is `:or_where` + + iex> alias Rummage.Ecto.Services.BuildSearchQuery + iex> import Ecto.Query + iex> queryable = from u in "parents" + #Ecto.Query + iex> BuildSearchQuery.handle_lt(queryable, :field_1, "field_!", :or_where) + #Ecto.Query + + When `search_expr` is `:not_where` + iex> alias Rummage.Ecto.Services.BuildSearchQuery iex> import Ecto.Query iex> queryable = from u in "parents" #Ecto.Query - iex> BuildSearchQuery.handle_lt(queryable, :field_1, "field_!") - #Ecto.Query + iex> BuildSearchQuery.handle_lt(queryable, :field_1, "field_!", :not_where) + #Ecto.Query= ^\"field_!\"> + """ - @spec handle_lt(Ecto.Query.t, atom, term) :: {Ecto.Query.t} - def handle_lt(queryable, field, search_term) do + @spec handle_lt(Ecto.Query.t(), atom(), term(), + __MODULE__.search_expr()) :: Ecto.Query.t() + def handle_lt(queryable, field, search_term, :where) do queryable |> where([..., b], field(b, ^field) < ^search_term) end + def handle_lt(queryable, field, search_term, :or_where) do + queryable + |> or_where([..., b], + field(b, ^field) < ^search_term) + end + def handle_lt(queryable, field, search_term, :not_where) do + queryable + |> where([..., b], + field(b, ^field) >= ^search_term) + end @doc """ - Builds a searched `queryable` on top of the given `queryable` using `field` and `search_type` - when the `search_term` is `gteq`. + Builds a searched `queryable` on top of the given `queryable` using + `field`, `search_term` and `search_expr` when the `search_type` is `gteq`. + + Assumes that `search_expr` is in #{inspect @search_exprs}. ## Examples + When `search_expr` is `:where` + + iex> alias Rummage.Ecto.Services.BuildSearchQuery + iex> import Ecto.Query + iex> queryable = from u in "parents" + #Ecto.Query + iex> BuildSearchQuery.handle_gteq(queryable, :field_1, "field_!", :where) + #Ecto.Query= ^\"field_!\"> + + When `search_expr` is `:or_where` + + iex> alias Rummage.Ecto.Services.BuildSearchQuery + iex> import Ecto.Query + iex> queryable = from u in "parents" + #Ecto.Query + iex> BuildSearchQuery.handle_gteq(queryable, :field_1, "field_!", :or_where) + #Ecto.Query= ^\"field_!\"> + + When `search_expr` is `:not_where` + iex> alias Rummage.Ecto.Services.BuildSearchQuery iex> import Ecto.Query iex> queryable = from u in "parents" #Ecto.Query - iex> BuildSearchQuery.handle_gteq(queryable, :field_1, "field_!") - #Ecto.Query= ^"field_!"> + iex> BuildSearchQuery.handle_gteq(queryable, :field_1, "field_!", :not_where) + #Ecto.Query + """ - @spec handle_gteq(Ecto.Query.t, atom, term) :: {Ecto.Query.t} - def handle_gteq(queryable, field, search_term) do + @spec handle_gteq(Ecto.Query.t(), atom(), term(), + __MODULE__.search_expr()) :: Ecto.Query.t() + def handle_gteq(queryable, field, search_term, :where) do queryable |> where([..., b], field(b, ^field) >= ^search_term) end + def handle_gteq(queryable, field, search_term, :or_where) do + queryable + |> or_where([..., b], + field(b, ^field) >= ^search_term) + end + def handle_gteq(queryable, field, search_term, :not_where) do + queryable + |> where([..., b], + field(b, ^field) < ^search_term) + end @doc """ - Builds a searched `queryable` on top of the given `queryable` using `field` and `search_type` - when the `search_term` is `lteq`. + Builds a searched `queryable` on top of the given `queryable` using + `field`, `search_term` and `search_expr` when the `search_type` is `lteq`. + + Assumes that `search_expr` is in #{inspect @search_exprs}. ## Examples + When `search_expr` is `:where` + + iex> alias Rummage.Ecto.Services.BuildSearchQuery + iex> import Ecto.Query + iex> queryable = from u in "parents" + #Ecto.Query + iex> BuildSearchQuery.handle_lteq(queryable, :field_1, "field_!", :where) + #Ecto.Query + + When `search_expr` is `:or_where` + + iex> alias Rummage.Ecto.Services.BuildSearchQuery + iex> import Ecto.Query + iex> queryable = from u in "parents" + #Ecto.Query + iex> BuildSearchQuery.handle_lteq(queryable, :field_1, "field_!", :or_where) + #Ecto.Query + + When `search_expr` is `:not_where` + iex> alias Rummage.Ecto.Services.BuildSearchQuery iex> import Ecto.Query iex> queryable = from u in "parents" #Ecto.Query - iex> BuildSearchQuery.handle_lteq(queryable, :field_1, "field_!") - #Ecto.Query + iex> BuildSearchQuery.handle_lteq(queryable, :field_1, "field_!", :not_where) + #Ecto.Query ^\"field_!\"> + """ - @spec handle_lteq(Ecto.Query.t, atom, term) :: {Ecto.Query.t} - def handle_lteq(queryable, field, search_term) do + @spec handle_lteq(Ecto.Query.t(), atom(), term(), + __MODULE__.search_expr()) :: Ecto.Query.t() + def handle_lteq(queryable, field, search_term, :where) do queryable |> where([..., b], field(b, ^field) <= ^search_term) end + def handle_lteq(queryable, field, search_term, :or_where) do + queryable + |> or_where([..., b], + field(b, ^field) <= ^search_term) + end + def handle_lteq(queryable, field, search_term, :not_where) do + queryable + |> where([..., b], + field(b, ^field) > ^search_term) + end @doc """ - Builds a searched `queryable` on `field` is_nil (when `term` is true), or not is_nil (when `term` is false). + Builds a searched `queryable` on `field` is_nil (when `term` is true), + or not is_nil (when `term` is false), based on `search_expr` given. + + Checkout [Ecto.Query.API.like/2](https://hexdocs.pm/ecto/Ecto.Query.API.html#is_nil/1) + for more info. + + Assumes that `search_expr` is in #{inspect @search_exprs}. ## Examples + When `search_expr` is `:where` + iex> alias Rummage.Ecto.Services.BuildSearchQuery iex> import Ecto.Query iex> queryable = from u in "parents" #Ecto.Query - iex> BuildSearchQuery.handle_is_null(queryable, :field_1, true) + iex> BuildSearchQuery.handle_is_null(queryable, :field_1, true, :where) #Ecto.Query - iex> BuildSearchQuery.handle_is_null(queryable, :field_1, false) + iex> BuildSearchQuery.handle_is_null(queryable, :field_1, false, :where) #Ecto.Query - """ - @spec handle_is_null(Ecto.Query.t, atom, term) :: {Ecto.Query.t} - def handle_is_null(queryable, field, term \\ true) - def handle_is_null(queryable, field, term) when term in ["true", true, 1] do + When `search_expr` is `:or_where` + + iex> alias Rummage.Ecto.Services.BuildSearchQuery + iex> import Ecto.Query + iex> queryable = from u in "parents" + #Ecto.Query + iex> BuildSearchQuery.handle_is_null(queryable, :field_1, true, :or_where) + #Ecto.Query + iex> BuildSearchQuery.handle_is_null(queryable, :field_1, false, :or_where) + #Ecto.Query + + When `search_expr` is `:not_where` + + iex> alias Rummage.Ecto.Services.BuildSearchQuery + iex> import Ecto.Query + iex> queryable = from u in "parents" + #Ecto.Query + iex> BuildSearchQuery.handle_is_null(queryable, :field_1, true, :not_where) + #Ecto.Query + iex> BuildSearchQuery.handle_is_null(queryable, :field_1, false, :not_where) + #Ecto.Query + + """ + @spec handle_is_null(Ecto.Query.t(), atom(), boolean(), + __MODULE__.search_expr()) :: Ecto.Query.t() + def handle_is_null(queryable, field, true, :where)do queryable |> where([..., b], is_nil(field(b, ^field))) end - def handle_is_null(queryable, field, term) when term in ["false", false, 0] do + def handle_is_null(queryable, field, true, :or_where)do + queryable + |> or_where([..., b], + is_nil(field(b, ^field))) + end + def handle_is_null(queryable, field, true, :not_where)do + queryable + |> where([..., b], + not is_nil(field(b, ^field))) + end + def handle_is_null(queryable, field, :false, :where) do queryable |> where([..., b], not is_nil(field(b, ^field))) end + def handle_is_null(queryable, field, :false, :or_where) do + queryable + |> or_where([..., b], + not is_nil(field(b, ^field))) + end + def handle_is_null(queryable, field, :false, :not_where) do + queryable + |> where([..., b], + is_nil(field(b, ^field))) + end end From 084c9961838e01424e4dce003eaa7fcf09bac62a Mon Sep 17 00:00:00 2001 From: Adi Date: Mon, 8 Jan 2018 19:22:16 -0500 Subject: [PATCH 34/64] Add boolean field to product for tests --- test/support/product.ex | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/test/support/product.ex b/test/support/product.ex index 722cdde..81b6f57 100644 --- a/test/support/product.ex +++ b/test/support/product.ex @@ -1,9 +1,14 @@ defmodule Rummage.Ecto.Product do + @moduledoc """ + This is Product Ecto.Schema for testing Rummage.Ecto with float values + and boolean values + """ use Ecto.Schema schema "products" do field :name, :string field :price, :float + field :available, :boolean belongs_to :category, Rummage.Ecto.Category timestamps() From 08d0eec8f598ea6de1d4570bed631ea9e9c8b316 Mon Sep 17 00:00:00 2001 From: Adi Date: Mon, 8 Jan 2018 19:22:33 -0500 Subject: [PATCH 35/64] Add moduledoc to category --- test/support/category.ex | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/support/category.ex b/test/support/category.ex index e928e0e..28dba00 100644 --- a/test/support/category.ex +++ b/test/support/category.ex @@ -1,4 +1,8 @@ defmodule Rummage.Ecto.Category do + @moduledoc """ + This is a Category Ecto.Schema for testing Rummage.Ecto with a nested + associations + """ use Ecto.Schema use Rummage.Ecto From dc12333178579f21d1447c623a1025992f87c4c4 Mon Sep 17 00:00:00 2001 From: Adi Date: Mon, 8 Jan 2018 19:22:47 -0500 Subject: [PATCH 36/64] Add available field to products for testing --- priv/repo/migrations/20170303043059_create_products.exs | 1 + 1 file changed, 1 insertion(+) diff --git a/priv/repo/migrations/20170303043059_create_products.exs b/priv/repo/migrations/20170303043059_create_products.exs index 2393890..2a1ac64 100644 --- a/priv/repo/migrations/20170303043059_create_products.exs +++ b/priv/repo/migrations/20170303043059_create_products.exs @@ -5,6 +5,7 @@ defmodule Rummage.Ecto.Repo.Migrations.CreateProducts do create table(:products) do add :name, :string add :price, :float + add :available, :boolean add :category_id, references(:categories) timestamps() From a5d7283eb44f1545b9bae0e2369fcad749348fea Mon Sep 17 00:00:00 2001 From: Adi Date: Mon, 8 Jan 2018 19:27:54 -0500 Subject: [PATCH 37/64] Update Search Hook with docs: - Make modifications to support `search_expr` - Add tests for the above changes - Add documentation to reflect the above changes --- lib/rummage_ecto/hooks/search.ex | 57 +++++++++++++++++++++++++++----- 1 file changed, 48 insertions(+), 9 deletions(-) diff --git a/lib/rummage_ecto/hooks/search.ex b/lib/rummage_ecto/hooks/search.ex index 3e4be8e..49fda94 100644 --- a/lib/rummage_ecto/hooks/search.ex +++ b/lib/rummage_ecto/hooks/search.ex @@ -89,9 +89,13 @@ defmodule Rummage.Ecto.Hooks.Search do value corresponding to that key is a list of params for that key, which should include the keys: `#{Enum.join(@expected_keys, ", ")}`. - This function expects a `search_type` and a list of `associations` (empty for none). - The `search_term` is what the `field` will be matched to based on the - `search_type`. + This function expects a `search_expr`, `search_type` and a list of + `associations` (empty for none). The `search_term` is what the `field` + will be matched to based on the `search_type` and `search_expr`. + + If no `search_expr` is given, it defaults to `where`. + + For all `search_exprs`, refer to `Rummage.Ecto.Services.BuildSearchQuery`. For all `search_types`, refer to `Rummage.Ecto.Services.BuildSearchQuery`. @@ -121,7 +125,7 @@ defmodule Rummage.Ecto.Hooks.Search do iex> alias Rummage.Ecto.Hooks.Search iex> import Ecto.Query iex> search_params = %{field1: %{assoc: [], - ...> search_type: "like", search_term: "field1"}} + ...> search_type: :like, search_term: "field1", search_expr: :where}} iex> Search.run(Rummage.Ecto.Product, search_params) #Ecto.Query @@ -130,7 +134,7 @@ defmodule Rummage.Ecto.Hooks.Search do iex> alias Rummage.Ecto.Hooks.Search iex> import Ecto.Query iex> search_params = %{field1: %{assoc: [], - ...> search_type: "like", search_term: "field1"}} + ...> search_type: :like, search_term: "field1", search_expr: :where}} iex> query = from p in "products" iex> Search.run(query, search_params) #Ecto.Query @@ -140,10 +144,10 @@ defmodule Rummage.Ecto.Hooks.Search do iex> alias Rummage.Ecto.Hooks.Search iex> import Ecto.Query iex> search_params = %{field1: %{assoc: [inner: "category"], - ...> search_type: "like", search_term: "field1"}} + ...> search_type: :like, search_term: "field1", search_expr: :or_where}} iex> query = from p in "products" iex> Search.run(query, search_params) - #Ecto.Query + #Ecto.Query When a valid map of params is passed with an `Ecto.Query.t`, with `assoc`s, with different join types: @@ -151,11 +155,45 @@ defmodule Rummage.Ecto.Hooks.Search do iex> alias Rummage.Ecto.Hooks.Search iex> import Ecto.Query iex> search_params = %{field1: %{assoc: [inner: "category", left: "category", cross: "category"], - ...> search_type: "like", search_term: "field1"}} + ...> search_type: :like, search_term: "field1", search_expr: :where}} iex> query = from p in "products" iex> Search.run(query, search_params) #Ecto.Query + When a valid map of params is passed with an `Ecto.Query.t`, searching on + a boolean param + + iex> alias Rummage.Ecto.Hooks.Search + iex> import Ecto.Query + iex> search_params = %{available: %{assoc: [], + ...> search_type: :eq, search_term: true, search_expr: :where}} + iex> query = from p in "products" + iex> Search.run(query, search_params) + #Ecto.Query + + When a valid map of params is passed with an `Ecto.Query.t`, searching on + a float param + + iex> alias Rummage.Ecto.Hooks.Search + iex> import Ecto.Query + iex> search_params = %{price: %{assoc: [], + ...> search_type: :gteq, search_term: 10.0, search_expr: :where}} + iex> query = from p in "products" + iex> Search.run(query, search_params) + #Ecto.Query= ^10.0> + + When a valid map of params is passed with an `Ecto.Query.t`, searching on + a boolean param, but with a wrong `search_type`. + NOTE: This doesn't validate the search_type of search_term + + iex> alias Rummage.Ecto.Hooks.Search + iex> import Ecto.Query + iex> search_params = %{available: %{assoc: [], + ...> search_type: :ilike, search_term: true, search_expr: :where}} + iex> query = from p in "products" + iex> Search.run(query, search_params) + ** (ArgumentError) argument error + """ @spec run(Ecto.Query.t(), map()) :: Ecto.Query.t() def run(q, s), do: handle_search(q, s) @@ -180,10 +218,11 @@ defmodule Rummage.Ecto.Hooks.Search do assocs = Map.get(field_params, :assoc) search_type = Map.get(field_params, :search_type) search_term = Map.get(field_params, :search_term) + search_expr = Map.get(field_params, :search_expr, :where) assocs |> Enum.reduce(from(e in subquery(queryable)), &join_by_assoc(&1, &2)) - |> BuildSearchQuery.run(field, search_type, search_term) + |> BuildSearchQuery.run(field, {search_expr, search_type}, search_term) end # Helper function which handles associations in a query with a join From a103e8631d917f19cee734c2ceb46b0a424928b4 Mon Sep 17 00:00:00 2001 From: Adi Date: Mon, 8 Jan 2018 19:36:55 -0500 Subject: [PATCH 38/64] Remove elixir v1.3.4 --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 361682f..24f9094 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,5 @@ language: elixir elixir: - - 1.3.4 - 1.4.5 - 1.5.2 otp_release: From 1afc45a6d2dc7d8de8714c07839a2756c6bf5295 Mon Sep 17 00:00:00 2001 From: Adi Date: Mon, 8 Jan 2018 19:41:52 -0500 Subject: [PATCH 39/64] Add hex.audit to travis.yml --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 24f9094..6b7cec0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,6 +10,7 @@ env: script: - mix test after_script: + - mix hex.audit - mix coveralls.travis - mix credo - mix deps.get --only docs From c69f2728d72960a72ed5a71d3a71dfb51bcd75d6 Mon Sep 17 00:00:00 2001 From: Adi Date: Mon, 8 Jan 2018 21:23:55 -0500 Subject: [PATCH 40/64] Update Search hook: - Update docs - Update assoc to take atoms - Add note with a function to be used in future --- lib/rummage_ecto/hooks/search.ex | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/lib/rummage_ecto/hooks/search.ex b/lib/rummage_ecto/hooks/search.ex index 49fda94..d9a830e 100644 --- a/lib/rummage_ecto/hooks/search.ex +++ b/lib/rummage_ecto/hooks/search.ex @@ -41,7 +41,7 @@ defmodule Rummage.Ecto.Hooks.Search do ``` - There are many other `search_types`. Check out `Rummage.Ecto.Services.BuildSearchQuery`'s docs + There are many other `search_types`. Check out `Rummage.Ecto.Services.BuildSearchQuery` docs to explore more `search_types` This module can be overridden with a custom module while using `Rummage.Ecto` @@ -143,7 +143,7 @@ defmodule Rummage.Ecto.Hooks.Search do iex> alias Rummage.Ecto.Hooks.Search iex> import Ecto.Query - iex> search_params = %{field1: %{assoc: [inner: "category"], + iex> search_params = %{field1: %{assoc: [inner: :category], ...> search_type: :like, search_term: "field1", search_expr: :or_where}} iex> query = from p in "products" iex> Search.run(query, search_params) @@ -154,7 +154,7 @@ defmodule Rummage.Ecto.Hooks.Search do iex> alias Rummage.Ecto.Hooks.Search iex> import Ecto.Query - iex> search_params = %{field1: %{assoc: [inner: "category", left: "category", cross: "category"], + iex> search_params = %{field1: %{assoc: [inner: :category, left: :category, cross: :category], ...> search_type: :like, search_term: "field1", search_expr: :where}} iex> query = from p in "products" iex> Search.run(query, search_params) @@ -228,9 +228,14 @@ defmodule Rummage.Ecto.Hooks.Search do # Helper function which handles associations in a query with a join # type. defp join_by_assoc({join, assoc}, query) do - join(query, join, [..., p1], p2 in assoc(p1, ^String.to_atom(assoc))) + join(query, join, [..., p1], p2 in assoc(p1, ^assoc)) end + # NOTE: These functions can be used in future for multiple search fields that + # are associated. + # defp applied_associations(queryable) when is_atom(queryable), do: [] + # defp applied_associations(queryable), do: Enum.map(queryable.joins, & Atom.to_string(elem(&1.assoc, 1))) + # Helper function that validates the list of params based on # @expected_keys list defp validate_params(params) do From edb03e9dfde0d27dfe10ec5cf54e974e92af9cb6 Mon Sep 17 00:00:00 2001 From: Adi Date: Mon, 8 Jan 2018 21:37:24 -0500 Subject: [PATCH 41/64] Update specs for Paginate.run/2 --- lib/rummage_ecto/hooks/paginate.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/rummage_ecto/hooks/paginate.ex b/lib/rummage_ecto/hooks/paginate.ex index 75f8caa..b0024a4 100644 --- a/lib/rummage_ecto/hooks/paginate.ex +++ b/lib/rummage_ecto/hooks/paginate.ex @@ -117,7 +117,7 @@ defmodule Rummage.Ecto.Hooks.Paginate do #Ecto.Query """ - @spec run(Ecto.Query.t, map) :: Ecto.Query.t + @spec run(Ecto.Query.t(), map()) :: Ecto.Query.t() def run(queryable, paginate_params) do :ok = validate_params(paginate_params) From 3d28092f86b35384861feb0938d7e3c8d8f4f50c Mon Sep 17 00:00:00 2001 From: Adi Date: Mon, 8 Jan 2018 21:37:36 -0500 Subject: [PATCH 42/64] Update Sort hook: - Make it work with new Hook structure - Add better documentation - Add cleaner functions - Use atoms over strings - Add doc tests for new sort operations --- lib/rummage_ecto/hooks/sort.ex | 293 +++++++++++++++------------------ 1 file changed, 134 insertions(+), 159 deletions(-) diff --git a/lib/rummage_ecto/hooks/sort.ex b/lib/rummage_ecto/hooks/sort.ex index 1681a64..df69cf9 100644 --- a/lib/rummage_ecto/hooks/sort.ex +++ b/lib/rummage_ecto/hooks/sort.ex @@ -1,7 +1,19 @@ defmodule Rummage.Ecto.Hooks.Sort do @moduledoc """ - `Rummage.Ecto.Hooks.Sort` is the default sort hook that comes shipped - with `Rummage.Ecto`. + TODO: Explain how to use `assoc` better + + `Rummage.Ecto.Hooks.Sort` is the default sort hook that comes with + `Rummage.Ecto`. + + This module provides a operations that can add sorting functionality to + a pipeline of `Ecto` queries. This module works by taking the `field` that should + be used to `order_by`, `order` which can be `asc` or `desc` and `assoc`, + which is a keyword list of assocations associated with those `fields`. + + NOTE: This module doesn't return a list of entries, but a `Ecto.Query.t`. + + + This module `uses` `Rummage.Ecto.Hook`. Usage: For a regular sort: @@ -12,7 +24,7 @@ defmodule Rummage.Ecto.Hooks.Sort do ```elixir alias Rummage.Ecto.Hooks.Sort - sorted_queryable = Sort.run(Parent, %{"sort" => %{"assoc" => [], "field" => "field_1.asc"}}) + sorted_queryable = Sort.run(Parent, %{assoc: [], field: :name, order: :asc}}) ``` For a case-insensitive sort: @@ -25,7 +37,7 @@ defmodule Rummage.Ecto.Hooks.Sort do ```elixir alias Rummage.Ecto.Hooks.Sort - sorted_queryable = Sort.run(Parent, %{"sort" => %{"assoc" => [], "field" => "field_1.asc.ci"}}) + sorted_queryable = Sort.run(Parent, %{assoc: [], field: :name, order: :asc, ci: true}}) ``` @@ -46,225 +58,188 @@ defmodule Rummage.Ecto.Hooks.Sort do .sort: CustomHook ``` - The `CustomHook` must implement behaviour `Rummage.Ecto.Hook`. For examples of `CustomHook`, check out some - `custom_hooks` that are shipped with elixir: `Rummage.Ecto.CustomHooks.SimpleSearch`, `Rummage.Ecto.CustomHooks.SimpleSort`, + The `CustomHook` must use `Rummage.Ecto.Hook`. For examples of `CustomHook`, + check out some `custom_hooks` that are shipped with `Rummage.Ecto`: + `Rummage.Ecto.CustomHooks.SimpleSearch`, `Rummage.Ecto.CustomHooks.SimpleSort`, Rummage.Ecto.CustomHooks.SimplePaginate """ + use Rummage.Ecto.Hook + import Ecto.Query - @behaviour Rummage.Ecto.Hook + @expected_keys ~w(field order assoc)a + @err_msg "Error in params, No values given for keys: " @doc """ - Builds a sort `queryable` on top of the given `queryable` from the rummage parameters - from the given `rummage` struct. + This is the callback implementation of `Rummage.Ecto.Hook.run/2`. - ## Examples - When rummage `struct` passed doesn't have the key `"sort"`, it simply returns the - `queryable` itself: + Builds a sort `Ecto.Query.t` on top of the given `Ecto.Queryable` variable + using given `params`. - iex> alias Rummage.Ecto.Hooks.Sort - iex> import Ecto.Query - iex> Sort.run(Parent, %{}) - Parent + Besides an `Ecto.Query.t` an `Ecto.Schema` module can also be passed as it + implements `Ecto.Queryable` - When the `queryable` passed is not just a `struct`: + Params is a `Map` which is expected to have the keys `#{Enum.join(@expected_keys, ", ")}`. - iex> alias Rummage.Ecto.Hooks.Sort - iex> import Ecto.Query - iex> queryable = from u in "parents" - #Ecto.Query - iex> Sort.run(queryable, %{}) - #Ecto.Query + This funciton expects a `field` atom, `order` which can be `asc` or `desc`, + `ci` which is a boolean indicating the case-insensitivity and `assoc` which + is a list of associations with their join types. - When rummage `struct` passed has the key `"sort"`, but with a value of `{}`, `""` - or `[]` it simply returns the `queryable` itself: + ## Examples + When an empty map is passed as `params`: iex> alias Rummage.Ecto.Hooks.Sort iex> import Ecto.Query - iex> Sort.run(Parent, %{"sort" => {}}) - Parent + iex> Sort.run(Parent, %{}) + ** (RuntimeError) Error in params, No values given for keys: field, order, assoc - iex> alias Rummage.Ecto.Hooks.Sort - iex> import Ecto.Query - iex> Sort.run(Parent, %{"sort" => ""}) - Parent + When a non-empty map is passed as `params`, but with a missing key: iex> alias Rummage.Ecto.Hooks.Sort iex> import Ecto.Query - iex> Sort.run(Parent, %{"sort" => %{}}) - Parent + iex> Sort.run(Parent, %{field: :name}) + ** (RuntimeError) Error in params, No values given for keys: order, assoc - When rummage `struct` passed has the key `"sort"`, but empty associations array - it just orders it by the passed `queryable`: + When a valid map of params is passed with an `Ecto.Schema` module: iex> alias Rummage.Ecto.Hooks.Sort iex> import Ecto.Query - iex> rummage = %{"sort" => %{"assoc" => [], "field" => "field_1.asc"}} - %{"sort" => %{"assoc" => [], - "field" => "field_1.asc"}} - iex> queryable = from u in "parents" - #Ecto.Query - iex> Sort.run(queryable, rummage) - #Ecto.Query + iex> Sort.run(Rummage.Ecto.Product, %{field: :name, assoc: [], order: :asc}) + #Ecto.Query - iex> alias Rummage.Ecto.Hooks.Sort - iex> import Ecto.Query - iex> rummage = %{"sort" => %{"assoc" => [], "field" => "field_1.desc"}} - %{"sort" => %{"assoc" => [], - "field" => "field_1.desc"}} - iex> queryable = from u in "parents" - #Ecto.Query - iex> Sort.run(queryable, rummage) - #Ecto.Query - - When no `order` is specified, it returns the `queryable` itself: + When the `queryable` passed is an `Ecto.Query` variable: iex> alias Rummage.Ecto.Hooks.Sort iex> import Ecto.Query - iex> rummage = %{"sort" => %{"assoc" => [], "field" => "field_1"}} - %{"sort" => %{"assoc" => [], - "field" => "field_1"}} - iex> queryable = from u in "parents" - #Ecto.Query - iex> Sort.run(queryable, rummage) - #Ecto.Query + iex> queryable = from u in "products" + #Ecto.Query + iex> Sort.run(queryable, %{field: :name, assoc: [], order: :asc}) + #Ecto.Query - When rummage `struct` passed has the key `"sort"`, with `field` and `order` - it returns a sorted version of the `queryable` passed in as the argument: + When the `queryable` passed is an `Ecto.Query` variable, with `desc` order: iex> alias Rummage.Ecto.Hooks.Sort iex> import Ecto.Query - iex> rummage = %{"sort" => %{"assoc" => ["parent", "parent"], "field" => "field_1.asc"}} - %{"sort" => %{"assoc" => ["parent", "parent"], "field" => "field_1.asc"}} - iex> queryable = from u in "parents" - #Ecto.Query - iex> Sort.run(queryable, rummage) - #Ecto.Query + iex> queryable = from u in "products" + #Ecto.Query + iex> Sort.run(queryable, %{field: :name, assoc: [], order: :desc}) + #Ecto.Query + When the `queryable` passed is an `Ecto.Query` variable, with `ci` true: iex> alias Rummage.Ecto.Hooks.Sort iex> import Ecto.Query - iex> rummage = %{"sort" => %{"assoc" => ["parent", "parent"], "field" => "field_1.desc"}} - %{"sort" => %{"assoc" => ["parent", "parent"], "field" => "field_1.desc"}} - iex> queryable = from u in "parents" - #Ecto.Query - iex> Sort.run(queryable, rummage) - #Ecto.Query + iex> queryable = from u in "products" + #Ecto.Query + iex> Sort.run(queryable, %{field: :name, assoc: [], order: :asc, ci: true}) + #Ecto.Query - When no `order` is specified even with the associations, it returns the `queryable` itself: + When the `queryable` passed is an `Ecto.Query` variable, with associations: iex> alias Rummage.Ecto.Hooks.Sort iex> import Ecto.Query - iex> rummage = %{"sort" => %{"assoc" => ["parent", "parent"], "field" => "field_1"}} - %{"sort" => %{"assoc" => ["parent", "parent"], - "field" => "field_1"}} - iex> queryable = from u in "parents" - #Ecto.Query - iex> Sort.run(queryable, rummage) - #Ecto.Query + iex> queryable = from u in "products" + #Ecto.Query + iex> Sort.run(queryable, %{field: :name, assoc: [inner: :category, left: :category], order: :asc}) + #Ecto.Query - When rummage `struct` passed has `case-insensitive` sort, it returns - a sorted version of the `queryable` with `case_insensitive` arguments: + When the `queryable` passed is an `Ecto.Schema` module with associations, + `desc` order and `ci` true: iex> alias Rummage.Ecto.Hooks.Sort iex> import Ecto.Query - iex> rummage = %{"sort" => %{"assoc" => ["parent", "parent"], "field" => "field_1.asc.ci"}} - %{"sort" => %{"assoc" => ["parent", "parent"], "field" => "field_1.asc.ci"}} - iex> queryable = from u in "parents" - #Ecto.Query - iex> Sort.run(queryable, rummage) - #Ecto.Query - """ - @spec run(Ecto.Query.t, map) :: {Ecto.Query.t, map} - def run(queryable, rummage) do - case Map.get(rummage, "sort") do - a when a in [nil, [], {}, [""], "", %{}] -> queryable - sort_params -> - sort_params = case sort_params["assoc"] do - s when s in [nil, ""] -> Map.put(sort_params, "assoc", []) - _ -> sort_params - end - - case Regex.match?(~r/\w.ci+$/, sort_params["field"]) do - true -> - order_param = sort_params["field"] - |> String.split(".") - |> Enum.drop(-1) - |> Enum.join(".") - - sort_params = {sort_params["assoc"], order_param} - - handle_sort(queryable, sort_params, true) - _ -> - handle_sort(queryable, {sort_params["assoc"], sort_params["field"]}) - end - end - end - - @doc """ - Implementation of `before_hook` for `Rummage.Ecto.Hooks.Sort`. This just returns back `rummage` at this point. - It doesn't matter what `queryable` or `opts` are, it just returns back `rummage`. - - ## Examples - iex> alias Rummage.Ecto.Hooks.Sort - iex> Sort.before_hook(Parent, %{}, %{}) - %{} + iex> queryable = Rummage.Ecto.Product + Rummage.Ecto.Product + iex> Sort.run(queryable, %{field: :name, assoc: [inner: :category], order: :desc, ci: true}) + #Ecto.Query """ - @spec before_hook(Ecto.Query.t, map, map) :: map - def before_hook(_queryable, rummage, _opts), do: rummage + @spec run(Ecto.Query.t(), map()) :: Ecto.Query.t() + def run(queryable, sort_params) do + :ok = validate_params(sort_params) - defp handle_sort(queryable, sort_params, ci \\ false) do - order_param = sort_params - |> elem(1) - - association_names = sort_params - |> elem(0) + handle_sort(queryable, sort_params) + end - queryable = from(e in subquery(queryable)) + # Helper function which handles addition of paginated query on top of + # the sent queryable variable + defp handle_sort(queryable, sort_params) do + order = Map.get(sort_params, :order) + field = Map.get(sort_params, :field) + assocs = Map.get(sort_params, :assoc) + ci = Map.get(sort_params, :ci, false) + + assocs + |> Enum.reduce(from(e in subquery(queryable)), &join_by_assoc(&1, &2)) + |> handle_ordering(field, order, ci) + end - association_names - |> Enum.reduce(queryable, &join_by_association(&1, &2)) - |> handle_ordering(order_param, ci) + # Helper function which handles associations in a query with a join + # type. + defp join_by_assoc({join, assoc}, query) do + join(query, join, [..., p1], p2 in assoc(p1, ^assoc)) end + # This is a helper macro to get case_insensitive query using fragments defmacrop case_insensitive(field) do quote do fragment("lower(?)", unquote(field)) end end + # NOTE: These functions can be used in future for multiple sort fields that + # are associated. # defp applied_associations(queryable) when is_atom(queryable), do: [] # defp applied_associations(queryable), do: Enum.map(queryable.joins, & Atom.to_string(elem(&1.assoc, 1))) - defp handle_ordering(queryable, order_param, ci) do - case Regex.match?(~r/\w.asc+$/, order_param) - or Regex.match?(~r/\w.desc+$/, order_param) do - true -> - parsed_field = order_param - |> String.split(".") - |> Enum.drop(-1) - |> Enum.join(".") - - order_type = order_param - |> String.split(".") - |> Enum.at(-1) - - queryable |> order_by_assoc(order_type, parsed_field, ci) - _ -> queryable - end + # Helper function that handles adding order_by to a query based on order type + # case insensitivity and field + defp handle_ordering(queryable, field, order, ci) do + order_by_assoc(queryable, order, field, ci) + end + + defp order_by_assoc(queryable, order_type, field, false) do + order_by(queryable, [p0, ..., p2], [{^order_type, field(p2, ^field)}]) end - defp join_by_association(association, queryable) do - join(queryable, :inner, [..., p1], p2 in assoc(p1, ^String.to_atom(association))) + defp order_by_assoc(queryable, order_type, field, true) do + order_by(queryable, [p0, ..., p2], + [{^order_type, case_insensitive(field(p2, ^field))}]) end - defp order_by_assoc(queryable, order_type, parsed_field, false) do - order_by(queryable, [p0, ..., p2], [{^String.to_atom(order_type), field(p2, ^String.to_atom(parsed_field))}]) + # Helper function that validates the list of params based on + # @expected_keys list + defp validate_params(params) do + key_validations = Enum.map(@expected_keys, &Map.fetch(params, &1)) + + case Enum.filter(key_validations, & &1 == :error) do + [] -> :ok + _ -> raise @err_msg <> missing_keys(key_validations) + end end - defp order_by_assoc(queryable, order_type, parsed_field, true) do - order_by(queryable, [p0, ..., p2], [{^String.to_atom(order_type), case_insensitive(field(p2, ^String.to_atom(parsed_field)))}]) + # Helper function used to build error message using missing keys + defp missing_keys(key_validations) do + key_validations + |> Enum.with_index() + |> Enum.filter(fn {v, _i} -> v == :error end) + |> Enum.map(fn {_v, i} -> Enum.at(@expected_keys, i) end) + |> Enum.map(&to_string/1) + |> Enum.join(", ") end + + @doc """ + Callback implementation for `Rummage.Ecto.Hook.format_params/2`. + + This just returns back `search_params` at this point. + It doesn't matter what `queryable` or `opts` are. + + ## Examples + iex> alias Rummage.Ecto.Hooks.Sort + iex> Sort.format_params(Parent, %{}, %{}) + %{} + """ + @spec format_params(Ecto.Query.t(), map(), keyword()) :: map() + def format_params(_queryable, sort_params, _opts), do: sort_params end From 1867c79a1a7450b0cedcfb35bcb327b660411ba9 Mon Sep 17 00:00:00 2001 From: Adi Date: Tue, 9 Jan 2018 00:55:17 -0500 Subject: [PATCH 43/64] Update Rummage.Ecto: - Update docs - Add more docs with help - Add doc tests - Steamline ways to use Config - Update function docs - Add macro docs - Add examples for macro docs --- lib/rummage_ecto.ex | 238 +++++++++++++++++++++++++++++++------------- 1 file changed, 169 insertions(+), 69 deletions(-) diff --git a/lib/rummage_ecto.ex b/lib/rummage_ecto.ex index 49be498..559c368 100644 --- a/lib/rummage_ecto.ex +++ b/lib/rummage_ecto.ex @@ -15,113 +15,213 @@ defmodule Rummage.Ecto do Usage: ```elixir - defmodule Rummage.Ecto.Product do + defmodule Rummage.Ecto.Category do use Ecto.Schema + use Rummage.Ecto + + schema "categories" do + field :name, :string + end end ``` This allows you to do: - iex> rummage = %{"search" => %{"name" => %{"assoc" => [], "search_type" => "ilike", "search_term" => "field_!"}}} - iex> {queryable, rummage} = Rummage.Ecto.rummage(Rummage.Ecto.Product, rummage) + iex> rummage = %{search: %{name: %{assoc: [], search_type: :ilike, search_term: "field_!"}}} + iex> {queryable, rummage} = Rummage.Ecto.Category.rummageq(Rummage.Ecto.Category, rummage) iex> queryable - #Ecto.Query + #Ecto.Query iex> rummage - %{"search" => %{"name" => %{"assoc" => [], "search_term" => "field_!", "search_type" => "ilike"}}} - - """ - - alias Rummage.Ecto.Config + %{search: %{name: %{assoc: [], search_expr: :where, + search_term: "field_!", search_type: :ilike}}} - @hooks [search: :default, sort: :default, paginate: :default] + This also allows you to do call `rummage/2` without a `queryable` which defaults + to the module calling `rummage`, which is `Rummage.Ecto.Category` in this case: - @doc """ - This is the function which calls to the `Rummage` `hooks`. It is the entry-point to `Rummage.Ecto`. - This function takes in a `queryable`, a `rummage` struct and an `opts` map. Possible `opts` values are: + iex> rummage = %{search: %{name: %{assoc: [], search_type: :ilike, search_term: "field_!"}}} + iex> {queryable, rummage} = Rummage.Ecto.Category.rummage(rummage) + iex> queryable + #Ecto.Query + iex> rummage + %{search: %{name: %{assoc: [], search_expr: :where, + search_term: "field_!", search_type: :ilike}}} - - `repo`: If you haven't set up a .repo`, or are using an app that uses multiple repos, this might come handy. - This overrides the .repo` in the configuration. + """ - - `search`: This allows us to override a `Rummage.Ecto.Hook` with a `CustomHook`. This `CustomHook` must implement - the behavior `Rummage.Ecto.Hook`. + alias Rummage.Ecto.Config, as: RConfig - - `sort`: + @doc """ + This is the function which calls to the `Rummage` `hooks`. + It is the entry-point to `Rummage.Ecto`. + + This function takes in a `queryable`, a `rummage` map and an `opts` keyword. + Recognized `opts` keys are: + + * `repo`: If you haven't set up a `repo` at the config level or `__using__` + level, this a way of passing `repo` to `rummage`. If you have + already configured your app to use a default `repo` and/or + specified the `repo` at `__using__` level, this is a way of + overriding those defaults. + + * `per_page`: If you haven't set up a `per_page` at the config level or `__using__` + level, this a way of passing `per_page` to `rummage`. If you have + already configured your app to use a default `per_page` and/or + specified the `per_page` at `__using__` level, this is a way of + overriding those defaults. + + * `search`: If you haven't set up a `search` at the config level or `__using__` + level, this a way of passing `search` to `rummage`. If you have + already configured your app to use a default `search` and/or + specified the `search` at `__using__` level, this is a way of + overriding those defaults. This can be used to override native + `Rummage.Ecto.Hooks.Search` to a custom hook. + + * `sort`: If you haven't set up a `sort` at the config level or `__using__` + level, this a way of passing `sort` to `rummage`. If you have + already configured your app to use a default `sort` and/or + specified the `sort` at `__using__` level, this is a way of + overriding those defaults. This can be used to override native + `Rummage.Ecto.Hooks.Sort` to a custom hook. + + * `paginate`: If you haven't set up a `paginate` at the config level or `__using__` + level, this a way of passing `paginate` to `rummage`. If you have + already configured your app to use a default `paginate` and/or + specified the `paginate` at `__using__` level, this is a way of + overriding those defaults. This can be used to override native + `Rummage.Ecto.Hooks.Paginate` to a custom hook. - - `paginate`: ## Examples - When no `repo` or `per_page` key is given in the `opts` map, it uses - the default values for repo and per_page: - - iex> rummage = %{"search" => %{}, "sort" => %{}, "paginate" => %{}} - iex> {queryable, rummage} = Rummage.Ecto.rummage(Rummage.Ecto.Product, rummage) # We have set a.repo in the configuration to Rummage.Ecto.Repo - iex> rummage - %{"paginate" => %{"max_page" => "0", "page" => "1", - "per_page" => "2", "total_count" => "0"}, "search" => %{}, - "sort" => %{}} - iex> queryable - #Ecto.Query - - When a `repo` key is given in the `opts` map: + When no hook params are given, it just returns the queryable and the params: - iex> rummage = %{"search" => %{}, "sort" => %{}, "paginate" => %{}} - iex> {queryable, rummage} = Rummage.Ecto.rummage(Rummage.Ecto.Product, rummage, repo: Rummage.Ecto.Repo) + iex> import Rummage.Ecto + iex> alias Rummage.Ecto.Product + iex> rummage = %{} + iex> {queryable, rummage} = rummage(Product, rummage) iex> rummage - %{"paginate" => %{"max_page" => "0", "page" => "1", - "per_page" => "2", "total_count" => "0"}, "search" => %{}, - "sort" => %{}} + %{} iex> queryable - #Ecto.Query - - - When a `per_page` key is given in the `opts` map: - - iex> rummage = %{"search" => %{}, "sort" => %{}, "paginate" => %{}} - iex> {queryable, rummage} = Rummage.Ecto.rummage(Rummage.Ecto.Product, rummage, per_page: 5) + Rummage.Ecto.Product + + When a hook param is given, it just returns the + `queryable` and the `params`: + + iex> import Rummage.Ecto + iex> alias Rummage.Ecto.Product + iex> rummage = %{} + iex> repo = Rummage.Ecto.Repo + iex> Ecto.Adapters.SQL.Sandbox.checkout(repo) + iex> opts = [paginate: Rummage.Ecto.Hooks.Paginate, repo: repo] + iex> {queryable, rummage} = rummage(Product, rummage) iex> rummage - %{"paginate" => %{"max_page" => "0", "page" => "1", - "per_page" => "5", "total_count" => "0"}, "search" => %{}, - "sort" => %{}} + %{} iex> queryable - #Ecto.Query - - When a `CustomHook` is given: - - iex> rummage = %{"search" => %{"name" => "x"}, "sort" => %{}, "paginate" => %{}} - iex> {queryable, rummage} = Rummage.Ecto.rummage(Rummage.Ecto.Product, rummage, search: Rummage.Ecto.CustomHooks.SimpleSearch) + Rummage.Ecto.Product + + + When a hook is given, with correspondng params, it updates and returns the + `queryable` and the `params` accordingly: + + iex> import Rummage.Ecto + iex> alias Rummage.Ecto.Product + iex> rummage = %{paginate: %{per_page: 1, page: 1}} + iex> repo = Rummage.Ecto.Repo + iex> Ecto.Adapters.SQL.Sandbox.checkout(repo) + iex> repo.insert!(%Product{name: "name"}) + iex> repo.insert!(%Product{name: "name2"}) + iex> opts = [paginate: Rummage.Ecto.Hooks.Paginate, + ...> repo: repo] + iex> {queryable, rummage} = rummage(Product, rummage, opts) iex> rummage - %{"paginate" => %{"max_page" => "0", "page" => "1", - "per_page" => "2", "total_count" => "0"}, - "search" => %{"name" => "x"}, "sort" => %{}} + %{paginate: %{max_page: 2, page: 1, per_page: 1, total_count: 2}} iex> queryable - #Ecto.Query - + #Ecto.Query """ - @spec rummage(Ecto.Query.t, map, map) :: {Ecto.Query.t, map} + @spec rummage(Ecto.Query.t(), map(), Keyword.t()) :: {Ecto.Query.t(), map()} def rummage(queryable, rummage, opts \\ []) - def rummage(queryable, rummage, _opts) when rummage == nil, do: {queryable, %{}} def rummage(queryable, rummage, opts) do - hooks = [opts[:search] || Rummage.Ecto.Config.search(), - opts[:sort] || Rummage.Ecto.Config.sort(), - opts[:paginate] || Rummage.Ecto.Config.paginate()] - Enum.reduce(hooks, {queryable, rummage}, &apply_mod(&1, &2, opts)) + hooks = [search: Keyword.get(opts, :search, RConfig.search()), + sort: Keyword.get(opts, :sort, RConfig.sort()), + paginate: Keyword.get(opts, :paginate, RConfig.paginate())] + + rummage = + Enum.reduce(hooks, rummage, &format_hook_params(&1, &2, queryable, opts)) + + {Enum.reduce(hooks, queryable, &run_hook(&1, &2, rummage)), rummage} end - defp apply_mod(mod, {queryable, rummage}, opts) do - {apply(mod, :run, [queryable, rummage]), - apply(mod, :before_hook, [queryable, rummage, opts])} + defp format_hook_params({_, nil}, rummage, _, _), do: rummage + defp format_hook_params({type, hook_mod}, rummage, queryable, opts) do + case Map.get(rummage, type) do + nil -> rummage + params -> Map.put(rummage, type, + apply(hook_mod, :format_params, [queryable, params, opts])) + end + end + + defp run_hook({_, nil}, queryable, _), do: queryable + defp run_hook({type, hook_mod}, queryable, rummage) do + case Map.get(rummage, type) do + nil -> queryable + params -> apply(hook_mod, :run, [queryable, params]) + end end @doc """ - TODO: Add some crazy docs! + This macro allows an `Ecto.Schema` to leverage rummage's features with + ease. This macro defines a function `rummage/2` which can be called on + the Module `using` this which delegates to `Rummage.Ecto.rummage/3`, but + before doing that it resolves the options with default values for `repo`, + `search` hook, `sort` hook and `paginate` hook. If `rummage/2` is called with + those options in form of keys given to the last argument `opts`, then it + sets those keys to what's given else it delegates it to the defaults + specficied by `__using__` macro. If no defaults are specified, then it + further delegates it to configurations. + + The function `rummage/2` takes in `rummage params` and `opts` and calls + `Rummage.Ecto.rummage/3` with whatever schema is calling it as the + `queryable`. + + This macro also defines a function `rummageq/3` where q implies `queryable`. + Therefore this function can take a `queryable` as the first argument. + + In this way this macro makes it very easy to use `Rummage.Ecto`. + + ## Usage: + + ### Basic Usage where a default repo is specified as options to the macro. + ```elixir + defmodule MyApp.MySchema do + use Ecto.Schema + use Rummage.Ecto, repo: MyApp.Repo, per_page: 10 + end + ``` + + ### Advanced Usage where search and sort hooks are overrident for this module. + ```elixir + defmodule MyApp.MySchema do + use Ecto.Schema + use Rummage.Ecto, repo: MyApp.Repo, per_page: 10, + search: CustomSearchModule, + sort: CustomSortModule + end + + This allows you do just do `MyApp.Schema.rummage(rummage_params)` with specific + `rummage_params` and add `Rummage.Ecto`'s power to your schema. + ``` + """ defmacro __using__(opts) do quote do alias Rummage.Ecto.Config, as: RConfig - def rummage(queryable, rummage, opts \\ []) do + def rummage(rummage, opts \\ []) do + Rummage.Ecto.rummage(__MODULE__, rummage, uniq_merge(opts, defaults())) + end + + def rummageq(queryable, rummage, opts \\ []) do Rummage.Ecto.rummage(queryable, rummage, uniq_merge(opts, defaults())) end From 579f855af5d8a4d56a8330c74426f7c616fab119 Mon Sep 17 00:00:00 2001 From: Adi Date: Tue, 9 Jan 2018 00:56:35 -0500 Subject: [PATCH 44/64] Update sort hook: - Update `format_params/3` to put assoc and order to params --- lib/rummage_ecto/hooks/sort.ex | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/lib/rummage_ecto/hooks/sort.ex b/lib/rummage_ecto/hooks/sort.ex index df69cf9..d1975d7 100644 --- a/lib/rummage_ecto/hooks/sort.ex +++ b/lib/rummage_ecto/hooks/sort.ex @@ -232,14 +232,18 @@ defmodule Rummage.Ecto.Hooks.Sort do @doc """ Callback implementation for `Rummage.Ecto.Hook.format_params/2`. - This just returns back `search_params` at this point. - It doesn't matter what `queryable` or `opts` are. + This function ensures that params for each field have keys `assoc`, `order1 + which are essential for running this hook module. ## Examples iex> alias Rummage.Ecto.Hooks.Sort - iex> Sort.format_params(Parent, %{}, %{}) - %{} + iex> Sort.format_params(Parent, %{}, []) + %{assoc: [], order: :asc} """ @spec format_params(Ecto.Query.t(), map(), keyword()) :: map() - def format_params(_queryable, sort_params, _opts), do: sort_params + def format_params(_queryable, sort_params, _opts) do + sort_params + |> Map.put_new(:assoc, []) + |> Map.put_new(:order, :asc) + end end From b62c3e34167804f13dcbbd5e4932aaf8f7bb6e02 Mon Sep 17 00:00:00 2001 From: Adi Date: Tue, 9 Jan 2018 00:57:08 -0500 Subject: [PATCH 45/64] Update Pagiante hook: - Update `format_params/3` to add per_page and page --- lib/rummage_ecto/hooks/paginate.ex | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/lib/rummage_ecto/hooks/paginate.ex b/lib/rummage_ecto/hooks/paginate.ex index b0024a4..5b537bd 100644 --- a/lib/rummage_ecto/hooks/paginate.ex +++ b/lib/rummage_ecto/hooks/paginate.ex @@ -53,6 +53,8 @@ defmodule Rummage.Ecto.Hooks.Paginate do @expected_keys ~w(per_page page)a @err_msg "Error in params, No values given for keys: " + @per_page 10 + @doc """ This is the callback implementation of `Rummage.Ecto.Hook.run/2`. @@ -171,19 +173,20 @@ defmodule Rummage.Ecto.Hooks.Paginate do ## Examples - When a `repo` isn't passed in `opts`: + When a `repo` isn't passed in `opts` it gives an error: iex> alias Rummage.Ecto.Hooks.Paginate iex> alias Rummage.Ecto.Category iex> Paginate.format_params(Category, %{per_page: 1, page: 1}, []) ** (RuntimeError) Expected key `repo` in `opts`, got [] - When `paginate_params` given aren't valid: + When `paginate_params` given aren't valid, it uses defaults to populate params: iex> alias Rummage.Ecto.Hooks.Paginate iex> alias Rummage.Ecto.Category - iex> Paginate.format_params(Category, %{}, []) - ** (RuntimeError) Error in params, No values given for keys: per_page, page + iex> Ecto.Adapters.SQL.Sandbox.checkout(Rummage.Ecto.Repo) + iex> Paginate.format_params(Category, %{}, [repo: Rummage.Ecto.Repo]) + %{max_page: 0, page: 1, per_page: 10, total_count: 0} When `paginate_params` and `opts` given are valid: @@ -282,7 +285,7 @@ defmodule Rummage.Ecto.Hooks.Paginate do """ @spec format_params(Ecto.Query.t(), map(), keyword()) :: map() def format_params(queryable, paginate_params, opts) do - :ok = validate_params(paginate_params) + paginate_params = populate_params(paginate_params, opts) case Keyword.get(opts, :repo) do nil -> raise "Expected key `repo` in `opts`, got #{inspect(opts)}" @@ -290,6 +293,14 @@ defmodule Rummage.Ecto.Hooks.Paginate do end end + # Helper function that populate the list of params based on + # @expected_keys list + defp populate_params(params, opts) do + params + |> Map.put_new(:per_page, Keyword.get(opts, :per_page, @per_page)) + |> Map.put_new(:page, 1) + end + # Helper function which gets formatted list of params including # page, per_page, total_count and max_page keys defp get_params(queryable, paginate_params, repo) do From 55fa095c185165e7b6cdcd518ed68448ac270649 Mon Sep 17 00:00:00 2001 From: Adi Date: Tue, 9 Jan 2018 00:57:50 -0500 Subject: [PATCH 46/64] Update Search hook: - Update `format_params/3` to put assoc, search_type and search_expr in params --- lib/rummage_ecto/hooks/search.ex | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/lib/rummage_ecto/hooks/search.ex b/lib/rummage_ecto/hooks/search.ex index d9a830e..fa7f8c5 100644 --- a/lib/rummage_ecto/hooks/search.ex +++ b/lib/rummage_ecto/hooks/search.ex @@ -260,14 +260,28 @@ defmodule Rummage.Ecto.Hooks.Search do @doc """ Callback implementation for `Rummage.Ecto.Hook.format_params/2`. - This just returns back `search_params` at this point. - It doesn't matter what `queryable` or `opts` are. + This function ensures that params for each field have keys `assoc`, `search_type` and + `search_expr` which are essential for running this hook module. ## Examples iex> alias Rummage.Ecto.Hooks.Search - iex> Search.format_params(Parent, %{}, %{}) - %{} + iex> Search.format_params(Parent, %{field: %{}}, []) + %{field: %{assoc: [], search_expr: :where, search_type: :eq}} """ @spec format_params(Ecto.Query.t(), map(), keyword()) :: map() - def format_params(_queryable, search_params, _opts), do: search_params + def format_params(_queryable, search_params, _opts) do + search_params + |> Map.to_list() + |> Enum.map(&put_keys/1) + |> Enum.into(%{}) + end + + defp put_keys({field, field_params}) do + field_params = field_params + |> Map.put_new(:assoc, []) + |> Map.put_new(:search_type, :eq) + |> Map.put_new(:search_expr, :where) + + {field, field_params} + end end From db37cc2b189debe852019ca2c66efdd01a5923d4 Mon Sep 17 00:00:00 2001 From: Adi Date: Tue, 9 Jan 2018 00:58:31 -0500 Subject: [PATCH 47/64] Add __using__ dispatch to Product --- test/support/product.ex | 1 + 1 file changed, 1 insertion(+) diff --git a/test/support/product.ex b/test/support/product.ex index 81b6f57..c0ec770 100644 --- a/test/support/product.ex +++ b/test/support/product.ex @@ -4,6 +4,7 @@ defmodule Rummage.Ecto.Product do and boolean values """ use Ecto.Schema + use Rummage.Ecto, per_page: 1 schema "products" do field :name, :string From 84ce13ac68fff79888ea7b0f8052e58b1ba47dbb Mon Sep 17 00:00:00 2001 From: Adi Date: Tue, 9 Jan 2018 00:58:47 -0500 Subject: [PATCH 48/64] Update rummage_ecto_test with all the cases --- test/rummage_ecto_test.exs | 200 +++++++++++++++---------------------- 1 file changed, 81 insertions(+), 119 deletions(-) diff --git a/test/rummage_ecto_test.exs b/test/rummage_ecto_test.exs index 57a7eab..8244bbc 100644 --- a/test/rummage_ecto_test.exs +++ b/test/rummage_ecto_test.exs @@ -31,40 +31,27 @@ defmodule Rummage.EctoTest do test "rummage call with paginate returns the correct results for Product" do create_categories_and_products() - rummage = %{ - "paginate" => %{ - "page" => "2", - }, - } + rummage = %{paginate: %{page: 2}} - {queryable, rummage} = Rummage.Ecto.rummage(Product, rummage) + {queryable, rummage} = Product.rummage(rummage) products = Repo.all(queryable) # Test length - assert length(products) == 2 + assert length(products) == 1 # Test rummage params assert rummage == %{ - "paginate" => %{ - "per_page" => "2", - "page" => "2", - "max_page" => "4", - "total_count" => "8", - }, + paginate: %{per_page: 1, page: 2, max_page: 8, total_count: 8} } end test "rummage call with paginate returns the correct results for Category" do create_categories_and_products() - rummage = %{ - "paginate" => %{ - "page" => "1", - }, - } + rummage = %{paginate: %{page: 2}} - {queryable, rummage} = Category.rummage(Category, rummage, per_page: 3) + {queryable, rummage} = Category.rummage(rummage, per_page: 3) categories = Repo.all(queryable) @@ -73,23 +60,16 @@ defmodule Rummage.EctoTest do # Test rummage params assert rummage == %{ - "paginate" => %{ - "per_page" => "3", - "page" => "1", - "max_page" => "3", - "total_count" => "8", - }, + paginate: %{per_page: 3, page: 2, max_page: 3, total_count: 8}, } end test "rummage call with sort without assoc params returns the correct results" do create_categories_and_products() - rummage = %{ - "sort" => %{"field" => "name.asc"} - } + rummage = %{sort: %{field: :name, order: :asc}} - {queryable, rummage} = Rummage.Ecto.rummage(Product, rummage) + {queryable, rummage} = Product.rummage(rummage) products = Repo.all(queryable) @@ -103,9 +83,7 @@ defmodule Rummage.EctoTest do assert Enum.all?(products_2, & &1.name == "Product 2") # Test rummage params - assert rummage == %{ - "sort" => %{"field" => "name.asc"} - } + assert rummage == %{sort: %{assoc: [], field: :name, order: :asc}} end test "rummage call with sort and assoc params returns the correct results" do @@ -139,11 +117,9 @@ defmodule Rummage.EctoTest do test "rummage call with search and search_type lteq returns the correct results" do create_categories_and_products() - rummage = %{ - "search" => %{"price" => %{"search_type" => "lteq", "search_term" => 10}} - } + rummage = %{search: %{price: %{search_type: :lteq, search_term: 10}}} - {queryable, rummage} = Rummage.Ecto.rummage(Product, rummage) + {queryable, rummage} = Product.rummage(rummage) products = Repo.all(queryable) @@ -155,18 +131,17 @@ defmodule Rummage.EctoTest do # Test rummage params assert rummage == %{ - "search" => %{"price" => %{"search_type" => "lteq", "search_term" => 10}} + search: %{price: %{search_type: :lteq, search_term: 10, + search_expr: :where, assoc: []}} } end test "rummage call with search and search_type eq returns the correct results" do create_categories_and_products() - rummage = %{ - "search" => %{"price" => %{"search_type" => "eq", "search_term" => 10}} - } + rummage = %{search: %{price: %{search_type: :eq, search_term: 10}}} - {queryable, rummage} = Rummage.Ecto.rummage(Product, rummage) + {queryable, rummage} = Product.rummage(rummage) products = Repo.all(queryable) @@ -178,7 +153,8 @@ defmodule Rummage.EctoTest do # Test rummage params assert rummage == %{ - "search" => %{"price" => %{"search_type" => "eq", "search_term" => 10}} + search: %{price: %{search_type: :eq, search_term: 10, assoc: [], + search_expr: :where}} } end @@ -186,10 +162,10 @@ defmodule Rummage.EctoTest do create_categories_and_products() rummage = %{ - "search" => %{"price" => %{"search_type" => "gteq", "search_term" => 10}} + search: %{price: %{search_type: :gteq, search_term: 10}} } - {queryable, rummage} = Rummage.Ecto.rummage(Product, rummage) + {queryable, rummage} = Product.rummage(rummage) products = Repo.all(queryable) @@ -201,7 +177,8 @@ defmodule Rummage.EctoTest do # Test rummage params assert rummage == %{ - "search" => %{"price" => %{"search_type" => "gteq", "search_term" => 10}} + search: %{price: %{search_type: :gteq, search_term: 10, assoc: [], + search_expr: :where}} } end @@ -209,10 +186,11 @@ defmodule Rummage.EctoTest do create_categories_and_products() rummage = %{ - "search" => %{"category_name" => %{"assoc" => ["category"], "search_type" => "like", "search_term" => "1"}} + search: %{category_name: %{assoc: [inner: :category], search_type: :like, + search_term: "1"}} } - {queryable, rummage} = Rummage.Ecto.rummage(Product, rummage) + {queryable, rummage} = Product.rummage(rummage) products = Repo.all(queryable) @@ -224,7 +202,8 @@ defmodule Rummage.EctoTest do # Test rummage params assert rummage == %{ - "search" => %{"category_name" => %{"assoc" => ["category"], "search_type" => "like", "search_term" => "1"}} + search: %{category_name: %{assoc: [inner: :category], search_type: :like, + search_term: "1", search_expr: :where}} } end @@ -232,14 +211,12 @@ defmodule Rummage.EctoTest do create_categories_and_products() rummage = %{ - "paginate" => %{ - "page" => "2", - }, - "search" => %{"price" => %{"search_type" => "lteq", "search_term" => 10}}, - "sort" => %{"field" => "name.asc"}, + paginate: %{page: 2, per_page: 2}, + search: %{price: %{search_type: :lteq, search_term: 10}}, + sort: %{field: :name, order: :asc}, } - {queryable, rummage} = Rummage.Ecto.rummage(Product, rummage) + {queryable, rummage} = Product.rummage(rummage) products = Repo.all(queryable) @@ -253,14 +230,10 @@ defmodule Rummage.EctoTest do # Test rummage params assert rummage == %{ - "search" => %{"price" => %{"search_type" => "lteq", "search_term" => 10}}, - "sort" => %{"field" => "name.asc"}, - "paginate" => %{ - "per_page" => "2", - "page" => "2", - "max_page" => "2", - "total_count" => "4", - }, + search: %{price: %{search_type: :lteq, search_term: 10, assoc: [], + search_expr: :where}}, + sort: %{field: :name, order: :asc, assoc: []}, + paginate: %{per_page: 2, page: 2, max_page: 4, total_count: 8}, } end @@ -268,14 +241,13 @@ defmodule Rummage.EctoTest do create_categories_and_products() rummage = %{ - "paginate" => %{ - "page" => "2", - }, - "search" => %{"category_name" => %{"assoc" => ["category"], "search_type" => "like", "search_term" => "1"}}, - "sort" => %{"assoc" => ["category"], "field" => "category_name.asc"} + paginate: %{page: 1, per_page: 2}, + search: %{category_name: %{assoc: [inner: :category], search_type: :like, + search_term: "1"}}, + sort: %{assoc: [inner: :category], field: :category_name, order: :asc} } - {queryable, rummage} = Rummage.Ecto.rummage(Product, rummage) + {queryable, rummage} = Product.rummage(rummage) products = Repo.all(queryable) @@ -287,14 +259,10 @@ defmodule Rummage.EctoTest do # Test rummage params assert rummage == %{ - "search" => %{"category_name" => %{"assoc" => ["category"], "search_term" => "1", "search_type" => "like"}}, - "sort" => %{"field" => "category_name.asc", "assoc" => ["category"]}, - "paginate" => %{ - "per_page" => "2", - "page" => "1", - "max_page" => "1", - "total_count" => "2", - }, + search: %{category_name: %{assoc: [inner: :category], search_term: "1", + search_type: :like, search_expr: :where}}, + sort: %{field: :category_name, order: :asc, assoc: [inner: :category]}, + paginate: %{per_page: 2, page: 1, max_page: 4, total_count: 8} } end @@ -302,19 +270,18 @@ defmodule Rummage.EctoTest do create_categories_and_products() rummage = %{ - "paginate" => %{ - "page" => "1", - }, - "search" => %{"category_name" => %{"assoc" => ["category", "category"], "search_type" => "like", "search_term" => "Parent"}}, - "sort" => %{"assoc" => ["category"], "field" => "category_name.asc"} + paginate: %{page: 1}, + search: %{category_name: %{assoc: [{:inner, :category}, {:inner, :category}], + search_type: :like, search_term: "Parent"}}, + sort: %{assoc: [{:inner, :category}], field: :category_name, order: :asc} } - {queryable, rummage} = Rummage.Ecto.rummage(Product, rummage) + {queryable, rummage} = Product.rummage(rummage) products = Repo.all(queryable) # Test length - assert length(products) == 2 + assert length(products) == 1 # Test search assert Enum.all?(Repo.preload(products, :category), & Repo.preload(&1.category, :category).category.category_name =~ "Parent Category") @@ -325,13 +292,14 @@ defmodule Rummage.EctoTest do # Test rummage params assert rummage == %{ - "search" => %{"category_name" => %{"assoc" => ["category", "category"], "search_term" => "Parent", "search_type" => "like"}}, - "sort" => %{"field" => "category_name.asc", "assoc" => ["category"]}, - "paginate" => %{ - "per_page" => "2", - "page" => "1", - "max_page" => "4", - "total_count" => "8", + search: %{category_name: %{assoc: [inner: :category, inner: :category], + search_term: "Parent", search_type: :like, search_expr: :where}}, + sort: %{field: :category_name, order: :asc, assoc: [inner: :category]}, + paginate: %{ + per_page: 1, + page: 1, + max_page: 8, + total_count: 8, }, } end @@ -340,14 +308,14 @@ defmodule Rummage.EctoTest do create_categories_and_products() rummage = %{ - "paginate" => %{ - "page" => "1", - }, - "search" => %{"category_name" => %{"assoc" => ["category", "category"], "search_type" => "like", "search_term" => "Parent"}}, - "sort" => %{"assoc" => ["category", "category"], "field" => "category_name.asc"} + paginate: %{page: 1, per_page: 2}, + search: %{category_name: %{assoc: [inner: :category, inner: :category], + search_type: :like, search_term: "Parent"}}, + sort: %{assoc: [inner: :category, inner: :category], + field: :category_name, order: :asc} } - {queryable, rummage} = Rummage.Ecto.rummage(Product, rummage) + {queryable, rummage} = Product.rummage(rummage) products = Repo.all(queryable) @@ -363,14 +331,11 @@ defmodule Rummage.EctoTest do # Test rummage params assert rummage == %{ - "search" => %{"category_name" => %{"assoc" => ["category", "category"], "search_term" => "Parent", "search_type" => "like"}}, - "sort" => %{"field" => "category_name.asc", "assoc" => ["category", "category"]}, - "paginate" => %{ - "per_page" => "2", - "page" => "1", - "max_page" => "4", - "total_count" => "8", - }, + search: %{category_name: %{assoc: [inner: :category, inner: :category], + search_term: "Parent", search_type: :like, search_expr: :where}}, + sort: %{field: :category_name, order: :asc, + assoc: [inner: :category, inner: :category]}, + paginate: %{per_page: 2, page: 1, max_page: 4, total_count: 8}, } end @@ -378,19 +343,19 @@ defmodule Rummage.EctoTest do create_categories_and_products() rummage = %{ - "paginate" => %{ - "page" => "1", - }, - "search" => %{"category_name" => %{"assoc" => ["category"], "search_type" => "like", "search_term" => "Category"}}, - "sort" => %{"assoc" => ["category", "category"], "field" => "category_name.asc"} + paginate: %{page: 1}, + search: %{category_name: %{assoc: [{:inner, :category}], + search_type: :like, search_term: "Category"}}, + sort: %{assoc: [{:inner, :category}, {:inner, :category}], + field: :category_name, order: :asc} } - {queryable, rummage} = Rummage.Ecto.rummage(Product, rummage) + {queryable, rummage} = Product.rummage(rummage) products = Repo.all(queryable) # Test length - assert length(products) == 2 + assert length(products) == 1 # Test search assert Enum.all?(Repo.preload(products, :category), & &1.category.category_name =~ "Category") @@ -401,14 +366,11 @@ defmodule Rummage.EctoTest do # Test rummage params assert rummage == %{ - "search" => %{"category_name" => %{"assoc" => ["category"], "search_term" => "Category", "search_type" => "like"}}, - "sort" => %{"field" => "category_name.asc", "assoc" => ["category", "category"]}, - "paginate" => %{ - "per_page" => "2", - "page" => "1", - "max_page" => "4", - "total_count" => "8", - }, + search: %{category_name: %{assoc: [inner: :category], + search_term: "Category", search_type: :like, search_expr: :where}}, + sort: %{field: :category_name, order: :asc, + assoc: [inner: :category, inner: :category]}, + paginate: %{per_page: 1, page: 1, max_page: 8, total_count: 8} } end end From f3a0af5d8969937434666c3a7f2e81c99aa5ec6f Mon Sep 17 00:00:00 2001 From: Adi Date: Tue, 9 Jan 2018 01:01:23 -0500 Subject: [PATCH 49/64] Update doctests for Rummage.Ecto: - Test `format_params/3` for a hook --- lib/rummage_ecto.ex | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/lib/rummage_ecto.ex b/lib/rummage_ecto.ex index 559c368..fd08186 100644 --- a/lib/rummage_ecto.ex +++ b/lib/rummage_ecto.ex @@ -104,20 +104,21 @@ defmodule Rummage.Ecto do iex> queryable Rummage.Ecto.Product - When a hook param is given, it just returns the + + When a hook param is given, with hook module it just returns the `queryable` and the `params`: iex> import Rummage.Ecto iex> alias Rummage.Ecto.Product - iex> rummage = %{} + iex> rummage = %{paginate: %{page: 1}} iex> repo = Rummage.Ecto.Repo iex> Ecto.Adapters.SQL.Sandbox.checkout(repo) iex> opts = [paginate: Rummage.Ecto.Hooks.Paginate, repo: repo] - iex> {queryable, rummage} = rummage(Product, rummage) + iex> {queryable, rummage} = rummage(Product, rummage, opts) iex> rummage - %{} + %{paginate: %{max_page: 0, page: 1, per_page: 10, total_count: 0}} iex> queryable - Rummage.Ecto.Product + #Ecto.Query When a hook is given, with correspondng params, it updates and returns the From 4f9c910daf061f58bea4e6c6f02130dcf009c85d Mon Sep 17 00:00:00 2001 From: Adi Date: Tue, 9 Jan 2018 08:22:42 -0500 Subject: [PATCH 50/64] Add test and documentation for `nil` hook case --- lib/rummage_ecto.ex | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/lib/rummage_ecto.ex b/lib/rummage_ecto.ex index fd08186..6ac555e 100644 --- a/lib/rummage_ecto.ex +++ b/lib/rummage_ecto.ex @@ -104,6 +104,17 @@ defmodule Rummage.Ecto do iex> queryable Rummage.Ecto.Product + When `nil` hook module is given, it just returns the queryable and the params: + + iex> import Rummage.Ecto + iex> alias Rummage.Ecto.Product + iex> rummage = %{paginate: %{page: 1}} + iex> {queryable, rummage} = rummage(Product, rummage, paginate: nil) + iex> rummage + %{paginate: %{page: 1}} + iex> queryable + Rummage.Ecto.Product + When a hook param is given, with hook module it just returns the `queryable` and the `params`: @@ -141,8 +152,7 @@ defmodule Rummage.Ecto do """ @spec rummage(Ecto.Query.t(), map(), Keyword.t()) :: {Ecto.Query.t(), map()} - def rummage(queryable, rummage, opts \\ []) - def rummage(queryable, rummage, opts) do + def rummage(queryable, rummage, opts \\ []) do hooks = [search: Keyword.get(opts, :search, RConfig.search()), sort: Keyword.get(opts, :sort, RConfig.sort()), paginate: Keyword.get(opts, :paginate, RConfig.paginate())] From 0caf5cb772955394a6238dd47aec3d96ef8a0af2 Mon Sep 17 00:00:00 2001 From: Adi Date: Tue, 9 Jan 2018 09:12:46 -0500 Subject: [PATCH 51/64] Remove unused imports --- lib/rummage_ecto/hooks/sort.ex | 4 ---- 1 file changed, 4 deletions(-) diff --git a/lib/rummage_ecto/hooks/sort.ex b/lib/rummage_ecto/hooks/sort.ex index d1975d7..f8f35b3 100644 --- a/lib/rummage_ecto/hooks/sort.ex +++ b/lib/rummage_ecto/hooks/sort.ex @@ -90,21 +90,18 @@ defmodule Rummage.Ecto.Hooks.Sort do When an empty map is passed as `params`: iex> alias Rummage.Ecto.Hooks.Sort - iex> import Ecto.Query iex> Sort.run(Parent, %{}) ** (RuntimeError) Error in params, No values given for keys: field, order, assoc When a non-empty map is passed as `params`, but with a missing key: iex> alias Rummage.Ecto.Hooks.Sort - iex> import Ecto.Query iex> Sort.run(Parent, %{field: :name}) ** (RuntimeError) Error in params, No values given for keys: order, assoc When a valid map of params is passed with an `Ecto.Schema` module: iex> alias Rummage.Ecto.Hooks.Sort - iex> import Ecto.Query iex> Sort.run(Rummage.Ecto.Product, %{field: :name, assoc: [], order: :asc}) #Ecto.Query @@ -149,7 +146,6 @@ defmodule Rummage.Ecto.Hooks.Sort do `desc` order and `ci` true: iex> alias Rummage.Ecto.Hooks.Sort - iex> import Ecto.Query iex> queryable = Rummage.Ecto.Product Rummage.Ecto.Product iex> Sort.run(queryable, %{field: :name, assoc: [inner: :category], order: :desc, ci: true}) From f97ebed9415f470ad46bb60cf78cd6a5e05d9881 Mon Sep 17 00:00:00 2001 From: Adi Date: Tue, 9 Jan 2018 09:13:14 -0500 Subject: [PATCH 52/64] Add KeySet Pagination to CustomHooks: - Add documentation - Add keyset logic - Add pk logic - Add custom docs --- .../custom_hooks/keyset_paginate.ex | 364 ++++++++++++------ 1 file changed, 246 insertions(+), 118 deletions(-) diff --git a/lib/rummage_ecto/custom_hooks/keyset_paginate.ex b/lib/rummage_ecto/custom_hooks/keyset_paginate.ex index 6a50780..9132851 100644 --- a/lib/rummage_ecto/custom_hooks/keyset_paginate.ex +++ b/lib/rummage_ecto/custom_hooks/keyset_paginate.ex @@ -1,9 +1,17 @@ defmodule Rummage.Ecto.CustomHooks.KeysetPaginate do @moduledoc """ - `Rummage.Ecto.CustomHooks.KeysetPaginate` is a custom paginate hook that comes shipped - with `Rummage.Ecto`. + `Rummage.Ecto.CustomHooks.KeysetPaginate` is an example of a Custom Hook that + comes with `Rummage.Ecto`. - This module can be used by overriding the default paginate module. This can be done + This module uses `keyset` pagination to add a pagination query expression + on top a given `Ecto.Queryable`. + + For more information on Keyset Pagination, check this [article](http://use-the-index-luke.com/no-offset) + + NOTE: This module doesn't return a list of entries, but a `Ecto.Query.t`. + + + This module can be used by overriding the default module. This can be done in the following ways: In the `Rummage.Ecto` call: @@ -17,183 +25,303 @@ defmodule Rummage.Ecto.CustomHooks.KeysetPaginate do ```elixir config :rummage_ecto, Rummage.Ecto, - _paginate: Rummage.Ecto.CustomHooks.KeysetPaginate + paginate: Rummage.Ecto.CustomHooks.KeysetPaginate + ``` + + OR + + When `using` Rummage.Ecto with an `Ecto.Schema`: + ```elixir + defmodule MySchema do + use Rummage.Ecto, repo: SomeRepo, + paginate: Rummage.Ecto.CustomHooks.KeysetPaginate + end ``` """ + use Rummage.Ecto.Hook + import Ecto.Query - alias Rummage.Ecto.Config - @behaviour Rummage.Ecto.Hook + @expected_keys ~w(per_page page last_seen_pk pk)a + @err_msg "Error in params, No values given for keys: " + + @per_page 10 @doc """ - Builds a paginate queryable on top of the given `queryable` from the rummage parameters - from the given `rummage` struct. + This is the callback implementation of `Rummage.Ecto.Hook.run/2`. + + Builds a paginate `Ecto.Query.t` on top of a given `Ecto.Query.t` variable + with given `params`. + + Besides an `Ecto.Query.t` an `Ecto.Schema` module can also be passed as it + implements `Ecto.Queryable` + + Params is a `Map` which is expected to have the keys `#{Enum.join(@expected_keys, ", ")}`. + + If an expected key isn't given, a `Runtime Error` is raised. ## Examples - When rummage struct passed doesn't have the key "paginate", it simply returns the - queryable itself: + When an empty map is passed as `params`: iex> alias Rummage.Ecto.CustomHooks.KeysetPaginate - iex> import Ecto.Query iex> KeysetPaginate.run(Parent, %{}) - Parent + ** (RuntimeError) Error in params, No values given for keys: per_page, page, last_seen_pk, pk - When the queryable passed is not just a struct: + When a non-empty map is passed as `params`, but with a missing key: iex> alias Rummage.Ecto.CustomHooks.KeysetPaginate - iex> import Ecto.Query - iex> queryable = from u in "parents" - #Ecto.Query - iex> KeysetPaginate.run(queryable, %{}) - #Ecto.Query + iex> KeysetPaginate.run(Parent, %{per_page: 10}) + ** (RuntimeError) Error in params, No values given for keys: page, last_seen_pk, pk - When rummage `struct` passed has the key `"paginate"`, but with a value of `%{}`, `""` - or `[]` it simply returns the `queryable` itself: + When a valid map of params is passed with an `Ecto.Schema` module: iex> alias Rummage.Ecto.CustomHooks.KeysetPaginate - iex> import Ecto.Query - iex> KeysetPaginate.run(Parent, %{"paginate" => %{}}) - Parent + iex> params = %{per_page: 10, page: 1, last_seen_pk: 0, pk: :id} + iex> KeysetPaginate.run(Rummage.Ecto.Product, params) + #Ecto.Query ^0, limit: ^10> - iex> alias Rummage.Ecto.CustomHooks.KeysetPaginate - iex> import Ecto.Query - iex> KeysetPaginate.run(Parent, %{"paginate" => ""}) - Parent + When the `queryable` passed is an `Ecto.Query` variable: iex> alias Rummage.Ecto.CustomHooks.KeysetPaginate iex> import Ecto.Query - iex> KeysetPaginate.run(Parent, %{"paginate" => []}) - Parent + iex> queryable = from u in "products" + #Ecto.Query + iex> params = %{per_page: 10, page: 1, last_seen_pk: 0, pk: :id} + iex> KeysetPaginate.run(queryable, params) + #Ecto.Query ^0, limit: ^10> - When rummage struct passed has the key "paginate", with "per_page" and "page" keys - it returns a paginated version of the queryable passed in as the argument: - iex> alias Rummage.Ecto.CustomHooks.KeysetPaginate - iex> import Ecto.Query - iex> rummage = %{"paginate" => %{"per_page" => "1", "page" => "1"}} - %{"paginate" => %{"page" => "1", "per_page" => "1"}} - iex> queryable = from u in "parents" - #Ecto.Query - iex> KeysetPaginate.run(queryable, rummage) - #Ecto.Query + More examples: iex> alias Rummage.Ecto.CustomHooks.KeysetPaginate iex> import Ecto.Query - iex> rummage = %{"paginate" => %{"per_page" => "5", "page" => "2"}} - %{"paginate" => %{"page" => "2", "per_page" => "5"}} - iex> queryable = from u in "parents" - #Ecto.Query - iex> KeysetPaginate.run(queryable, rummage) - #Ecto.Query - - When no `"page"` key is passed, it defaults to `1`: + iex> params = %{per_page: 5, page: 5, last_seen_pk: 25, pk: :id} + iex> queryable = from u in "products" + #Ecto.Query + iex> KeysetPaginate.run(queryable, params) + #Ecto.Query ^25, limit: ^5> iex> alias Rummage.Ecto.CustomHooks.KeysetPaginate iex> import Ecto.Query - iex> rummage = %{"paginate" => %{"per_page" => "10"}} - %{"paginate" => %{"per_page" => "10"}} - iex> queryable = from u in "parents" - #Ecto.Query - iex> KeysetPaginate.run(queryable, rummage) - #Ecto.Query + iex> params = %{per_page: 5, page: 1, last_seen_pk: 0, pk: :some_id} + iex> queryable = from u in "products" + #Ecto.Query + iex> KeysetPaginate.run(queryable, params) + #Ecto.Query ^0, limit: ^5> + """ - @spec run(Ecto.Query.t, map) :: {Ecto.Query.t, map} - def run(queryable, rummage) do - paginate_params = Map.get(rummage, "paginate") + @spec run(Ecto.Query.t(), map()) :: Ecto.Query.t() + def run(queryable, paginate_params) do + :ok = validate_params(paginate_params) + + handle_paginate(queryable, paginate_params) + end - case paginate_params do - a when a in [nil, [], {}, [""], "", %{}] -> queryable - _ -> handle_paginate(queryable, paginate_params) + # Helper function which handles addition of paginated query on top of + # the sent queryable variable + defp handle_paginate(queryable, paginate_params) do + per_page = Map.get(paginate_params, :per_page) + last_seen_pk = Map.get(paginate_params, :last_seen_pk) + pk = Map.get(paginate_params, :pk) + + queryable + |> where([p1, ...], field(p1, ^pk) > ^last_seen_pk) + |> limit(^per_page) + end + + # Helper function that validates the list of params based on + # @expected_keys list + defp validate_params(params) do + key_validations = Enum.map(@expected_keys, &Map.fetch(params, &1)) + + case Enum.filter(key_validations, & &1 == :error) do + [] -> :ok + _ -> raise @err_msg <> missing_keys(key_validations) end end + # Helper function used to build error message using missing keys + defp missing_keys(key_validations) do + key_validations + |> Enum.with_index() + |> Enum.filter(fn {v, _i} -> v == :error end) + |> Enum.map(fn {_v, i} -> Enum.at(@expected_keys, i) end) + |> Enum.map(&to_string/1) + |> Enum.join(", ") + end + @doc """ - Implementation of `before_hook` for `Rummage.Ecto.CustomHooks.KeysetPaginate`. This function - takes a `queryable`, `rummage` struct and an `opts` map. Using those it calculates - the `total_count` and `max_page` for the paginate hook. + Callback implementation for `Rummage.Ecto.Hook.format_params/2`. + + This function takes an `Ecto.Query.t` or `queryable`, `paginate_params` which + will be passed to the `run/2` function, but also takes a list of options, + `opts`. + + The function expects `opts` to include a `repo` key which points to the + `Ecto.Repo` which will be used to calculate the `total_count` and `max_page` + for this paginate hook module. + ## Examples + + When a `repo` isn't passed in `opts` it gives an error: + iex> alias Rummage.Ecto.CustomHooks.KeysetPaginate iex> alias Rummage.Ecto.Category - iex> KeysetPaginate.before_hook(Category, %{}, %{}) - %{} + iex> KeysetPaginate.format_params(Category, %{per_page: 1, page: 1}, []) + ** (RuntimeError) Expected key `repo` in `opts`, got [] + + When `paginate_params` given aren't valid, it uses defaults to populate params: iex> alias Rummage.Ecto.CustomHooks.KeysetPaginate iex> alias Rummage.Ecto.Category iex> Ecto.Adapters.SQL.Sandbox.checkout(Rummage.Ecto.Repo) - iex> Rummage.Ecto.Repo.insert(%Category{category_name: "Category 1"}) - iex> Rummage.Ecto.Repo.insert(%Category{category_name: "Category 2"}) - iex> Rummage.Ecto.Repo.insert(%Category{category_name: "Category 3"}) - iex> rummage = %{"paginate" => %{"per_page" => "1", "page" => "1"}} - iex> KeysetPaginate.before_hook(Category, rummage, %{}) - %{"paginate" => %{"max_page" => "3", "page" => "1", "per_page" => "1", "total_count" => "3"}} - """ - @spec before_hook(Ecto.Query.t, map, map) :: map - def before_hook(queryable, rummage, opts) do - paginate_params = Map.get(rummage, "paginate") + iex> KeysetPaginate.format_params(Category, %{}, [repo: Rummage.Ecto.Repo]) + %{max_page: 0, page: 1, per_page: 10, total_count: 0, pk: :id, + last_seen_pk: 0} - case paginate_params do - nil -> rummage - _ -> - total_count = get_total_count(queryable, opts) + When `paginate_params` and `opts` given are valid: - {page, per_page} = parse_page_and_per_page(paginate_params, opts) + iex> alias Rummage.Ecto.CustomHooks.KeysetPaginate + iex> alias Rummage.Ecto.Category + iex> paginate_params = %{ + ...> per_page: 1, + ...> page: 1 + ...> } + iex> repo = Rummage.Ecto.Repo + iex> Ecto.Adapters.SQL.Sandbox.checkout(repo) + iex> KeysetPaginate.format_params(Category, paginate_params, [repo: repo]) + %{max_page: 0, last_seen_pk: 0, page: 1, + per_page: 1, total_count: 0, pk: :id} + + When `paginate_params` and `opts` given are valid: - per_page = if per_page < 1, do: 1, else: per_page + iex> alias Rummage.Ecto.CustomHooks.KeysetPaginate + iex> alias Rummage.Ecto.Category + iex> paginate_params = %{ + ...> per_page: 1, + ...> page: 1 + ...> } + iex> repo = Rummage.Ecto.Repo + iex> Ecto.Adapters.SQL.Sandbox.checkout(repo) + iex> repo.insert!(%Category{category_name: "name"}) + iex> repo.insert!(%Category{category_name: "name2"}) + iex> KeysetPaginate.format_params(Category, paginate_params, [repo: repo]) + %{max_page: 2, last_seen_pk: 0, page: 1, + per_page: 1, total_count: 2, pk: :id} + + When `paginate_params` and `opts` given are valid and when the `queryable` + passed has a `primary_key` defaulted to `id`. - max_page_fl = total_count / per_page - max_page = max_page_fl - |> Float.ceil - |> round + iex> alias Rummage.Ecto.CustomHooks.KeysetPaginate + iex> alias Rummage.Ecto.Category + iex> paginate_params = %{ + ...> per_page: 1, + ...> page: 1 + ...> } + iex> repo = Rummage.Ecto.Repo + iex> Ecto.Adapters.SQL.Sandbox.checkout(repo) + iex> repo.insert!(%Category{category_name: "name"}) + iex> repo.insert!(%Category{category_name: "name2"}) + iex> KeysetPaginate.format_params(Category, paginate_params, [repo: repo]) + %{max_page: 2, last_seen_pk: 0, page: 1, + per_page: 1, total_count: 2, pk: :id} + + When `paginate_params` and `opts` given are valid and when the `queryable` + passed has a custom `primary_key`. - page = cond do - page < 1 -> 1 - max_page > 0 && page > max_page -> max_page - true -> page - end + iex> alias Rummage.Ecto.CustomHooks.KeysetPaginate + iex> alias Rummage.Ecto.Item + iex> paginate_params = %{ + ...> per_page: 1, + ...> page: 2 + ...> } + iex> repo = Rummage.Ecto.Repo + iex> Ecto.Adapters.SQL.Sandbox.checkout(repo) + iex> repo.insert!(%Item{item_id: 5}) + iex> repo.insert!(%Item{item_id: 6}) + iex> KeysetPaginate.format_params(Item, paginate_params, [repo: repo]) + %{max_page: 2, last_seen_pk: 1, page: 2, + per_page: 1, total_count: 2, pk: :item_id} - paginate_params = paginate_params - |> Map.put("page", Integer.to_string(page)) - |> Map.put("per_page", Integer.to_string(per_page)) - |> Map.put("total_count", Integer.to_string(total_count)) - |> Map.put("max_page", Integer.to_string(max_page)) + """ + @spec format_params(Ecto.Query.t(), map(), keyword()) :: map() + def format_params(queryable, paginate_params, opts) do + paginate_params = populate_params(queryable, paginate_params, opts) - Map.put(rummage, "paginate", paginate_params) + case Keyword.get(opts, :repo) do + nil -> raise "Expected key `repo` in `opts`, got #{inspect(opts)}" + repo -> get_params(queryable, paginate_params, repo) end end - defp get_total_count(queryable, opts), do: length(apply(get_repo(opts), :all, [queryable])) + # Helper function that populate the list of params based on + # @expected_keys list + defp populate_params(queryable, params, opts) do + params = params + |> Map.put_new(:per_page, Keyword.get(opts, :per_page, @per_page)) + |> Map.put_new(:pk, pk(queryable)) + |> Map.put_new(:page, 1) - defp get_repo(opts) do - opts[:repo] || Config.repo + Map.put_new(params, :last_seen_pk, get_last_seen(params)) end - defp parse_page_and_per_page(paginate_params, opts) do - per_page = paginate_params - |> Map.get("per_page", Integer.to_string(opts[:per_page] || Config.per_page)) - |> String.to_integer - - page = paginate_params - |> Map.get("page", "1") - |> String.to_integer + # Helper function which gets the default last_seen_pk from + # page and per_page + defp get_last_seen(params) do + Map.get(params, :per_page) * (Map.get(params, :page) - 1) + end - {page, per_page} + # Helper function which gets formatted list of params including + # page, per_page, total_count and max_page keys + defp get_params(queryable, paginate_params, repo) do + per_page = Map.get(paginate_params, :per_page) + total_count = get_total_count(queryable, repo) + max_page = total_count + |> (& &1 / per_page).() + |> Float.ceil() + |> trunc() + + %{page: Map.get(paginate_params, :page), pk: Map.get(paginate_params, :pk), + last_seen_pk: Map.get(paginate_params, :last_seen_pk), + per_page: per_page, total_count: total_count, max_page: max_page} end - defp handle_paginate(queryable, paginate_params) do - per_page = paginate_params - |> Map.get("per_page") - |> String.to_integer + # Helper function which gets total count of a queryable based on + # the given repo. + # This excludes operations such as select, preload and order_by + # to make the query more effectient + defp get_total_count(queryable, repo) do + queryable + |> exclude(:select) + |> exclude(:preload) + |> exclude(:order_by) + |> get_count(repo, pk(queryable)) + end - page = paginate_params - |> Map.get("page", "1") - |> String.to_integer + # This function gets count of a query and repo passed. + # When primary key passed is nil, it just gets all the elements + # and counts them, but when a primary key is passed it just counts + # the distinct primary keys + defp get_count(query, repo, nil) do + repo + |> apply(:all, [distinct(query, :true)]) + |> Enum.count() + end + defp get_count(query, repo, pk) do + query = select(query, [s], count(field(s, ^pk), :distinct)) + hd(apply(repo, :all, [query])) + end - offset = per_page * (page - 1) + # Helper function which returns the primary key associated with a + # Queryable. + defp pk(queryable) do + schema = is_map(queryable) && elem(queryable.from, 1) || queryable - queryable - |> limit(^per_page) - |> offset(^offset) + case schema.__schema__(:primary_key) do + [] -> nil + list -> hd(list) + end end end From 846e0c4c2a96cb73f57ba4f704be9df37c178ee5 Mon Sep 17 00:00:00 2001 From: Adi Date: Tue, 9 Jan 2018 09:17:53 -0500 Subject: [PATCH 53/64] Add assumptions and notes to Keyset Pagination --- lib/rummage_ecto/custom_hooks/keyset_paginate.ex | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/lib/rummage_ecto/custom_hooks/keyset_paginate.ex b/lib/rummage_ecto/custom_hooks/keyset_paginate.ex index 9132851..d86696b 100644 --- a/lib/rummage_ecto/custom_hooks/keyset_paginate.ex +++ b/lib/rummage_ecto/custom_hooks/keyset_paginate.ex @@ -10,6 +10,19 @@ defmodule Rummage.Ecto.CustomHooks.KeysetPaginate do NOTE: This module doesn't return a list of entries, but a `Ecto.Query.t`. + # ASSUMPTIONS/NOTES: + + * This Hook assumes that the querried `Ecto.Schema` has a `primary_key`. + * This Hook also orders the query by ascending `primary_key` + + # When to Use KeysetPaginate? + + - Keyset Pagination is mainly here to make pagination faster for complex + pages. It is recommended that you use `Rummage.Ecto.Hooks.Paginate` for a + simple pagination operation, as this module has a lot of assumptions and + it's own ordering on top of the given query. + + NOTE: __It is not recommended to use this with the native sort hook__ This module can be used by overriding the default module. This can be done in the following ways: From 2983deb5bb8f61a629f91224a9249ca5f5f9c82f Mon Sep 17 00:00:00 2001 From: Adi Date: Tue, 9 Jan 2018 12:40:26 -0500 Subject: [PATCH 54/64] Update documentation of Search hook: - Add assumptions and notes - Add notes about advanced usage --- lib/rummage_ecto/hooks/search.ex | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/lib/rummage_ecto/hooks/search.ex b/lib/rummage_ecto/hooks/search.ex index fa7f8c5..2f173c8 100644 --- a/lib/rummage_ecto/hooks/search.ex +++ b/lib/rummage_ecto/hooks/search.ex @@ -11,6 +11,25 @@ defmodule Rummage.Ecto.Hooks.Search do NOTE: This module doesn't return a list of entries, but a `Ecto.Query.t`. + # ASSUMPTIONS/NOTES: + + * This Hook has the default `search_type` of `:ilike`, which is + case-insensitive. + * This Hook has the default `search_expr` of `:where`. + * This Hook assumes that the field passed is a field on the `Ecto.Schema` + that corresponds to the last association in the `assoc` list or the `Ecto.Schema` + that corresponds to the `from` in `queryable`, if `assoc` is an empty list. + + NOTE: It is adviced to not use multiple associated searches in one operation + as `assoc` still has some minor bugs when used with multiple searches. If you + need to use two searches with associations, I would pipe the call to another + search operation: + + ```elixir + Search.run(queryable, %{field1: %{assoc: [inner: :some_assoc]}} + |> Search.run(%{field2: %{assoc: [inner: :some_assoc2]}} + ``` + This module `uses` `Rummage.Ecto.Hook`. @@ -23,7 +42,8 @@ defmodule Rummage.Ecto.Hooks.Search do ```elixir alias Rummage.Ecto.Hooks.Search - searched_queryable = Search.run(Parent, %{field_1: %{assoc: [], search_type: "like", search_term: "field_!"}}}) + searched_queryable = Search.run(Parent, %{field_1: %{assoc: [], + search_type: :like, search_term: "field_!"}}}) ``` @@ -37,7 +57,8 @@ defmodule Rummage.Ecto.Hooks.Search do ```elixir alias Rummage.Ecto.Hooks.Search - searched_queryable = Search.run(Parent, %{field_1: %{assoc: [], search_type: "ilike", search_term: "field_!"}}}) + searched_queryable = Search.run(Parent, %{field_1: %{assoc: [], + search_type: :ilike, search_term: "field_!"}}}) ``` @@ -56,7 +77,7 @@ defmodule Rummage.Ecto.Hooks.Search do Globally for all models in `config.exs`: ```elixir - config :rummage_ecto, + config :my_app, Rummage.Ecto, .search: CustomHook ``` From 99f29e495d470daf88069d3c587b1ca13cdf98d3 Mon Sep 17 00:00:00 2001 From: Adi Date: Tue, 9 Jan 2018 13:00:40 -0500 Subject: [PATCH 55/64] Add better moduledoc for Search: - Explain how `assoc` works. - Add examples for `assoc` fields. - Add better doc details --- lib/rummage_ecto/hooks/search.ex | 78 ++++++++++++++++++++++++++++++-- 1 file changed, 74 insertions(+), 4 deletions(-) diff --git a/lib/rummage_ecto/hooks/search.ex b/lib/rummage_ecto/hooks/search.ex index 2f173c8..320b233 100644 --- a/lib/rummage_ecto/hooks/search.ex +++ b/lib/rummage_ecto/hooks/search.ex @@ -1,7 +1,5 @@ defmodule Rummage.Ecto.Hooks.Search do @moduledoc """ - TODO: Explain how to use `assoc` better - `Rummage.Ecto.Hooks.Search` is the default search hook that comes with `Rummage.Ecto`. @@ -10,6 +8,78 @@ defmodule Rummage.Ecto.Hooks.Search do `search_term` and `assoc` associated with those `fields`. NOTE: This module doesn't return a list of entries, but a `Ecto.Query.t`. + This module `uses` `Rummage.Ecto.Hook`. + + _____________________________________________________________________________ + + # ABOUT: + + ## Arguments: + + This Hook expects a `queryable` (an `Ecto.Queryable`) and + `search_params` (a `Map`). The map should be in the format: + `%{field_name: %{assoc: [], search_term: true, search_type: :eq}}` + + Details: + + * `field_name`: The field name to search by. + * `assoc`: List of associations in the search. + * `search_term`: Term to compare the `field_name` against. + * `search_type`: Determines the kind of search to perform. If `:eq`, it + expects the `field_name`'s value to be equal to `search_term`, + If `lt`, it expects it to be less than `search_term`. + To see all the `search_type`s, check `Rummage.Ecto.Services.BuildSearchQuery` + * `search_expr`: This is optional. Defaults to `:where`. This is the way current + search expression is appended to the existing query. + To see all the `search_expr`s, check `Rummage.Ecto.Services.BuildSearchQuery` + + + For example, if we want to search products with `available` = `true`, we would + do the following: + + ```elixir + Rummage.Ecto.Hooks.Search.run(Product, %{available: %{assoc: [], + search_type: :eq, + search_term: true}} + ``` + + This can be used for a search with multiple fields as well. Say, we want to + search for products that are `available`, but have a price less than `10.0`. + + ```elixir + Rummage.Ecto.Hooks.Search.run(Product, + %{available: %{assoc: [], + search_type: :eq, + search_term: true}, + %{price: %{assoc: [], + search_type: :lt, + search_term: 10.0}} + ``` + + ## Assoications: + + Assocaitions can be given to this module's run function as a key corresponding + to params associated with a field. For example, if we want to search products + that belong to a category with category_name, "super", we would do the + following: + + ```elixir + category_name_params = %{assoc: [inner: :category], search_term: "super", + search_type: :eq, search_expr: :where} + + Rummage.Ecto.Hooks.Search.run(Product, %{category_name: category_name_params}) + ``` + + The above operation will return an `Ecto.Query.t` struct which represents + a query equivalent to: + + ```elixir + from p in Product + |> join(:inner, :category) + |> where([p, c], c.category_name == ^"super") + ``` + + ____________________________________________________________________________ # ASSUMPTIONS/NOTES: @@ -30,10 +100,10 @@ defmodule Rummage.Ecto.Hooks.Search do |> Search.run(%{field2: %{assoc: [inner: :some_assoc2]}} ``` + ____________________________________________________________________________ - This module `uses` `Rummage.Ecto.Hook`. + # USAGE: - Usage: For a regular search: This returns a `queryable` which upon running will give a list of `Parent`(s) From 47edef5b97e96cd3f0da4bf03fcb33394c76e1b9 Mon Sep 17 00:00:00 2001 From: Adi Date: Tue, 9 Jan 2018 13:12:23 -0500 Subject: [PATCH 56/64] Update docs to show proper configuration --- lib/rummage_ecto/custom_hooks/keyset_paginate.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/rummage_ecto/custom_hooks/keyset_paginate.ex b/lib/rummage_ecto/custom_hooks/keyset_paginate.ex index d86696b..76458a2 100644 --- a/lib/rummage_ecto/custom_hooks/keyset_paginate.ex +++ b/lib/rummage_ecto/custom_hooks/keyset_paginate.ex @@ -36,7 +36,7 @@ defmodule Rummage.Ecto.CustomHooks.KeysetPaginate do Globally for all models in `config.exs`: ```elixir - config :rummage_ecto, + config :my_app, Rummage.Ecto, paginate: Rummage.Ecto.CustomHooks.KeysetPaginate ``` From 4de70983e83005496d499f8308f9cfcac09729d7 Mon Sep 17 00:00:00 2001 From: Adi Date: Tue, 9 Jan 2018 13:12:47 -0500 Subject: [PATCH 57/64] Add SimpleSearch without assoc: - Add docs - Add usage, association docs, assumptions and notes - Add configuration docs - Add funtion docs - Add doc tests - Add cleanup and stopped using config --- .../custom_hooks/simple_search.ex | 358 +++++++++++++----- 1 file changed, 271 insertions(+), 87 deletions(-) diff --git a/lib/rummage_ecto/custom_hooks/simple_search.ex b/lib/rummage_ecto/custom_hooks/simple_search.ex index 973ff2b..4e8c798 100644 --- a/lib/rummage_ecto/custom_hooks/simple_search.ex +++ b/lib/rummage_ecto/custom_hooks/simple_search.ex @@ -1,9 +1,104 @@ defmodule Rummage.Ecto.CustomHooks.SimpleSearch do @moduledoc """ - `Rummage.Ecto.CustomHooks.SimpleSearch` is a custom search hook that comes shipped - with `Rummage.Ecto`. + `Rummage.Ecto.CustomHooks.SimpleSearch` is an example of a Custom Hook that + comes with `Rummage.Ecto`. + + This module provides a operations that can add searching functionality to + a pipeline of `Ecto` queries. This module works by taking fields, and `search_type`, + `search_term` and `assoc` associated with those `fields`. + + This module doesn't support associations and hence is a simple alternative + to Rummage's default search hook. + + NOTE: This module doesn't return a list of entries, but a `Ecto.Query.t`. + This module `uses` `Rummage.Ecto.Hook`. + + _____________________________________________________________________________ + + # ABOUT: + + ## Arguments: + + This Hook expects a `queryable` (an `Ecto.Queryable`) and + `search_params` (a `Map`). The map should be in the format: + `%{field_name: %{search_term: true, search_type: :eq}}` + + Details: + + * `field_name`: The field name to search by. + * `search_term`: Term to compare the `field_name` against. + * `search_type`: Determines the kind of search to perform. If `:eq`, it + expects the `field_name`'s value to be equal to `search_term`, + If `lt`, it expects it to be less than `search_term`. + To see all the `search_type`s, check `Rummage.Ecto.Services.BuildSearchQuery` + * `search_expr`: This is optional. Defaults to `:where`. This is the way current + search expression is appended to the existing query. + To see all the `search_expr`s, check `Rummage.Ecto.Services.BuildSearchQuery` + + + For example, if we want to search products with `available` = `true`, we would + do the following: + + ```elixir + Rummage.Ecto.CustomHooks.SimpleSearch.run(Product, %{available: %{search_type: :eq, + search_term: true}} + ``` + + This can be used for a search with multiple fields as well. Say, we want to + search for products that are `available`, but have a price less than `10.0`. + + ```elixir + Rummage.Ecto.CustomHooks.SimpleSearch.run(Product, + %{available: %{search_type: :eq, + search_term: true}, + %{price: %{search_type: :lt, + search_term: 10.0}} + ``` + + ## Assoications: + + This module doesn't support assocations. + + ____________________________________________________________________________ + + + # ASSUMPTIONS/NOTES: + + * This Hook assumes that the searched field is a part of the schema passed + as the `queryable`. + * This Hook has the default `search_type` of `:eq`. + * This Hook has the default `search_expr` of `:where`. + + This module can be used by overriding the default module. This can be done + in the following ways: + + In the `Rummage.Ecto` call: + ```elixir + Rummage.Ecto.rummage(queryable, rummage, search: Rummage.Ecto.CustomHooks.SimpleSearch) + or + MySchema.rummage(rummage, search: Rummage.Ecto.CustomHooks.SimpleSearch) + ``` + + OR + + Globally for all models in `config.exs`: + ```elixir + config :my_app, + Rummage.Ecto, + search: Rummage.Ecto.CustomHooks.SimpleSearch + ``` + + OR + + When `using` Rummage.Ecto with an `Ecto.Schema`: + ```elixir + defmodule MySchema do + use Rummage.Ecto, repo: SomeRepo, + search: Rummage.Ecto.CustomHooks.SimpleSearch + end + + ## Usage: - Usage: For a regular search: This returns a `queryable` which upon running will give a list of `Parent`(s) @@ -12,7 +107,8 @@ defmodule Rummage.Ecto.CustomHooks.SimpleSearch do ```elixir alias Rummage.Ecto.CustomHooks.SimpleSearch - searched_queryable = SimpleSearch.run(Parent, %{"search" => %{"field_1" => "field_!"}}) + searched_queryable = SimpleSearch.run(Parent, %{field_1: %{search_type: :like, search_term: "field_!"}}}) + ``` For a case-insensitive search: @@ -25,15 +121,19 @@ defmodule Rummage.Ecto.CustomHooks.SimpleSearch do ```elixir alias Rummage.Ecto.CustomHooks.SimpleSearch - searched_queryable = SimpleSearch.run(Parent, %{"search" => %{"field_1.ci" => "field_!"}}) + searched_queryable = SimpleSearch.run(Parent, %{field_1: %{ search_type: "ilike", search_term: "field_!"}}}) + ``` - This module can be used by overriding the default search module. This can be done - in the following ways: + There are many other `search_types`. Check out `Rummage.Ecto.Services.BuildSearchQuery` docs + to explore more `search_types` - In the `Rummage.Ecto` call: + This module can be overridden with a custom module while using `Rummage.Ecto` + in `Ecto` struct module: + + In the `Ecto` module: ```elixir - Rummage.Ecto.rummage(queryable, rummage, search: Rummage.Ecto.CustomHooks.SimpleSearch) + Rummage.Ecto.rummage(queryable, rummage, search: CustomHook) ``` OR @@ -42,131 +142,215 @@ defmodule Rummage.Ecto.CustomHooks.SimpleSearch do ```elixir config :rummage_ecto, Rummage.Ecto, - .search: Rummage.Ecto.CustomHooks.SimpleSearch + .search: CustomHook ``` + + The `CustomHook` must use `Rummage.Ecto.Hook`. For examples of `CustomHook`, + check out some `custom_hooks` that are shipped with `Rummage.Ecto`: + `Rummage.Ecto.CustomHooks.SimpleSearch`, `Rummage.Ecto.CustomHooks.SimpleSort`, + Rummage.Ecto.CustomHooks.SimplePaginate """ + use Rummage.Ecto.Hook + import Ecto.Query - @behaviour Rummage.Ecto.Hook + @expected_keys ~w(search_type search_term)a + @err_msg "Error in params, No values given for keys: " - @doc """ - Builds a search queryable on top of the given `queryable` from the rummage parameters - from the given `rummage` struct. + alias Rummage.Ecto.Services.BuildSearchQuery + + @doc ~S""" + This is the callback implementation of `Rummage.Ecto.Hook.run/2`. + + Builds a search `Ecto.Query.t` on top of a given `Ecto.Query.t` variable + with given `params`. + + Besides an `Ecto.Query.t` an `Ecto.Schema` module can also be passed as it + implements `Ecto.Queryable` + + Params is a `Map`, keys of which are field names which will be searched for and + value corresponding to that key is a list of params for that key, which + should include the keys: `#{Enum.join(@expected_keys, ", ")}`. + + This function expects a `search_expr`, `search_type`. + The `search_term` is what the `field` + will be matched to based on the `search_type` and `search_expr`. + If no `search_expr` is given, it defaults to `where`. + + For all `search_exprs`, refer to `Rummage.Ecto.Services.BuildSearchQuery`. + + For all `search_types`, refer to `Rummage.Ecto.Services.BuildSearchQuery`. + + If an expected key isn't given, a `Runtime Error` is raised. + + NOTE:This hook isn't responsible for doing type validations. That's the + responsibility of the user sending `search_term` and `search_type`. ## Examples - When rummage struct passed doesn't have the key "search", it simply returns the - queryable itself: + When search_params are empty, it simply returns the same `queryable`: iex> alias Rummage.Ecto.CustomHooks.SimpleSearch iex> import Ecto.Query iex> SimpleSearch.run(Parent, %{}) Parent - When the queryable passed is not just a struct: + When a non-empty map is passed as a field `params`, but with a missing key: iex> alias Rummage.Ecto.CustomHooks.SimpleSearch iex> import Ecto.Query - iex> queryable = from u in "parents" - #Ecto.Query - iex> SimpleSearch.run(queryable, %{}) - #Ecto.Query + iex> SimpleSearch.run(Parent, %{field: %{search_type: :eq}}) + ** (RuntimeError) Error in params, No values given for keys: search_term - When rummage `struct` passed has the key `"search"`, but with a value of `%{}`, `""` - or `[]` it simply returns the `queryable` itself: + When a valid map of params is passed with an `Ecto.Schema` module: iex> alias Rummage.Ecto.CustomHooks.SimpleSearch iex> import Ecto.Query - iex> SimpleSearch.run(Parent, %{"search" => %{}}) - Parent + iex> search_params = %{field1: %{ + ...> search_type: :like, + ...> search_term: "field1", + ...> search_expr: :where}} + iex> SimpleSearch.run(Rummage.Ecto.Product, search_params) + #Ecto.Query + + When a valid map of params is passed with an `Ecto.Query.t`: iex> alias Rummage.Ecto.CustomHooks.SimpleSearch iex> import Ecto.Query - iex> SimpleSearch.run(Parent, %{"search" => ""}) - Parent + iex> search_params = %{field1: %{ + ...> search_type: :like, + ...> search_term: "field1", + ...> search_expr: :where}} + iex> query = from p in "products" + iex> SimpleSearch.run(query, search_params) + #Ecto.Query + + When a valid map of params is passed with an `Ecto.Query.t` and `:on_where`: iex> alias Rummage.Ecto.CustomHooks.SimpleSearch iex> import Ecto.Query - iex> SimpleSearch.run(Parent, %{"search" => []}) - Parent + iex> search_params = %{field1: %{ + ...> search_type: :like, + ...> search_term: "field1", + ...> search_expr: :or_where}} + iex> query = from p in "products" + iex> SimpleSearch.run(query, search_params) + #Ecto.Query - When rummage struct passed has the key "search", with "field" and "term" - it returns a searched version of the queryable passed in as the argument: + When a valid map of params is passed with an `Ecto.Query.t`, searching on + a boolean param iex> alias Rummage.Ecto.CustomHooks.SimpleSearch iex> import Ecto.Query - iex> rummage = %{"search" => %{"field_1" => "field_!"}} - %{"search" => %{"field_1" => "field_!"}} - iex> queryable = from u in "parents" - #Ecto.Query - iex> SimpleSearch.run(queryable, rummage) - #Ecto.Query + iex> search_params = %{available: %{ + ...> search_type: :eq, + ...> search_term: true, + ...> search_expr: :where}} + iex> query = from p in "products" + iex> SimpleSearch.run(query, search_params) + #Ecto.Query - When rummage struct passed has case-insensitive search, it returns - a searched version of the queryable with case_insensitive arguments: + When a valid map of params is passed with an `Ecto.Query.t`, searching on + a float param + + iex> alias Rummage.Ecto.CustomHooks.SimpleSearch + iex> import Ecto.Query + iex> search_params = %{price: %{ + ...> search_type: :gteq, + ...> search_term: 10.0, + ...> search_expr: :where}} + iex> query = from p in "products" + iex> SimpleSearch.run(query, search_params) + #Ecto.Query= ^10.0> + + When a valid map of params is passed with an `Ecto.Query.t`, searching on + a boolean param, but with a wrong `search_type`. + NOTE: This doesn't validate the search_type of search_term iex> alias Rummage.Ecto.CustomHooks.SimpleSearch iex> import Ecto.Query - iex> rummage = %{"search" => %{"field_1.ci" => "field_!"}} - %{"search" => %{"field_1.ci" => "field_!"}} - iex> queryable = from u in "parents" - #Ecto.Query - iex> SimpleSearch.run(queryable, rummage) - #Ecto.Query + iex> search_params = %{available: %{ + ...> search_type: :ilike, + ...> search_term: true, + ...> search_expr: :where}} + iex> query = from p in "products" + iex> SimpleSearch.run(query, search_params) + ** (ArgumentError) argument error + """ - @spec run(Ecto.Query.t, map) :: {Ecto.Query.t, map} - def run(queryable, rummage) do - search_params = Map.get(rummage, "search") + @spec run(Ecto.Query.t(), map()) :: Ecto.Query.t() + def run(q, s), do: handle_search(q, s) + + # Helper function which handles addition of search query on top of + # the sent queryable variable, for all search fields. + defp handle_search(queryable, search_params) do + search_params + |> Map.to_list() + |> Enum.reduce(queryable, &search_queryable(&1, &2)) + end + + # Helper function which handles addition of search query on top of + # the sent queryable variable, for ONE search fields. + # This delegates the query building to `BuildSearchQuery` module + defp search_queryable(param, queryable) do + field = elem(param, 0) + field_params = elem(param, 1) - case search_params do - a when a in [nil, [], {}, ""] -> queryable - _ -> handle_search(queryable, search_params) + :ok = validate_params(field_params) + + search_type = Map.get(field_params, :search_type) + search_term = Map.get(field_params, :search_term) + search_expr = Map.get(field_params, :search_expr, :where) + + BuildSearchQuery.run(from(e in subquery(queryable)), + field, {search_expr, search_type}, search_term) + end + + # Helper function that validates the list of params based on + # @expected_keys list + defp validate_params(params) do + key_validations = Enum.map(@expected_keys, &Map.fetch(params, &1)) + + case Enum.filter(key_validations, & &1 == :error) do + [] -> :ok + _ -> raise @err_msg <> missing_keys(key_validations) end end + # Helper function used to build error message using missing keys + defp missing_keys(key_validations) do + key_validations + |> Enum.with_index() + |> Enum.filter(fn {v, _i} -> v == :error end) + |> Enum.map(fn {_v, i} -> Enum.at(@expected_keys, i) end) + |> Enum.map(&to_string/1) + |> Enum.join(", ") + end + @doc """ - Implementation of `before_hook` for `Rummage.Ecto.CustomHooks.SimpleSearch`. This just returns back `rummage` at this point. - It doesn't matter what `queryable` or `opts` are, it just returns back `rummage`. + Callback implementation for `Rummage.Ecto.Hook.format_params/2`. + + This function ensures that params for each field have keys `assoc`, `search_type` and + `search_expr` which are essential for running this hook module. ## Examples iex> alias Rummage.Ecto.CustomHooks.SimpleSearch - iex> SimpleSearch.before_hook(Parent, %{}, %{}) - %{} + iex> SimpleSearch.format_params(Parent, %{field: %{}}, []) + %{field: %{search_expr: :where, search_type: :eq}} """ - @spec before_hook(Ecto.Query.t, map, map) :: map - def before_hook(_queryable, rummage, _opts), do: rummage - - defp handle_search(queryable, search_params) do + @spec format_params(Ecto.Query.t(), map(), keyword()) :: map() + def format_params(_queryable, search_params, _opts) do search_params - |> Map.to_list - |> Enum.reduce(queryable, &search_queryable(&1, &2)) + |> Map.to_list() + |> Enum.map(&put_keys/1) + |> Enum.into(%{}) end - defp search_queryable(param, queryable) do - field = param - |> elem(0) - - case Regex.match?(~r/\w.ci+$/, field) do - true -> - field = field - |> String.split(".") - |> Enum.drop(-1) - |> Enum.join(".") - |> String.to_atom - - term = elem(param, 1) - - queryable - |> where([b], - ilike(field(b, ^field), ^"%#{String.replace(term, "%", "\\%")}%")) - _ -> - field = String.to_atom(field) - - term = elem(param, 1) - - queryable - |> where([b], - like(field(b, ^field), ^"%#{String.replace(term, "%", "\\%")}%")) - end + defp put_keys({field, field_params}) do + field_params = field_params + |> Map.put_new(:search_type, :eq) + |> Map.put_new(:search_expr, :where) + + {field, field_params} end end From 11a079d2d2bf41e6d826cdf51e0ce2a07ecc5977 Mon Sep 17 00:00:00 2001 From: Adi Date: Tue, 9 Jan 2018 13:35:58 -0500 Subject: [PATCH 58/64] Update docs for KeysetPaginate and SimpleSearch: - Add better examples - Add better about docs --- .../custom_hooks/keyset_paginate.ex | 56 ++++++++++-- .../custom_hooks/simple_search.ex | 85 ++++++++----------- 2 files changed, 86 insertions(+), 55 deletions(-) diff --git a/lib/rummage_ecto/custom_hooks/keyset_paginate.ex b/lib/rummage_ecto/custom_hooks/keyset_paginate.ex index 76458a2..717dc27 100644 --- a/lib/rummage_ecto/custom_hooks/keyset_paginate.ex +++ b/lib/rummage_ecto/custom_hooks/keyset_paginate.ex @@ -6,16 +6,41 @@ defmodule Rummage.Ecto.CustomHooks.KeysetPaginate do This module uses `keyset` pagination to add a pagination query expression on top a given `Ecto.Queryable`. - For more information on Keyset Pagination, check this [article](http://use-the-index-luke.com/no-offset) + For more information on Keyset Pagination, check this + [article](http://use-the-index-luke.com/no-offset) NOTE: This module doesn't return a list of entries, but a `Ecto.Query.t`. + This module `uses` `Rummage.Ecto.Hook`. - # ASSUMPTIONS/NOTES: + _____________________________________________________________________________ - * This Hook assumes that the querried `Ecto.Schema` has a `primary_key`. - * This Hook also orders the query by ascending `primary_key` + # ABOUT: + + ## Arguments: + + This Hook expects a `queryable` (an `Ecto.Queryable`) and + `paginate_params` (a `Map`). The map should be in the format: + `%{per_page: 10, page: 1, last_seen_pk: 10, pk: :id}` + + Details: + + * `per_page`: Specifies the entries in each page. + * `page`: Specifies the `page` number. + * `last_seen_pk`: Specifies the primary_key value of last_seen entry, + This hook uses this entry instead of offset. + * `pk`: Specifies what's the `primary_key` for the entries being paginated. + Cannot be `nil` + + + For example, if we want to paginate products (primary_key = :id), we would + do the following: + + ```elixir + Rummage.Ecto.CustomHooks.KeysetPaginate.run(Product, + %{per_page: 10, page: 1, last_seen_pk: 10, pk: :id} + ``` - # When to Use KeysetPaginate? + ## When to Use KeysetPaginate? - Keyset Pagination is mainly here to make pagination faster for complex pages. It is recommended that you use `Rummage.Ecto.Hooks.Paginate` for a @@ -24,12 +49,31 @@ defmodule Rummage.Ecto.CustomHooks.KeysetPaginate do NOTE: __It is not recommended to use this with the native sort hook__ + _____________________________________________________________________________ + + # ASSUMPTIONS/NOTES: + + * This Hook assumes that the querried `Ecto.Schema` has a `primary_key`. + * This Hook also orders the query by ascending `primary_key` + + _____________________________________________________________________________ + + # USAGE + + ```elixir + alias Rummage.Ecto.CustomHooks.KeysetPaginate + + queryable = KeysetPaginate.run(Parent, + %{per_page: 10, page: 1, last_seen_pk: 10, pk: :id}) + ``` + This module can be used by overriding the default module. This can be done in the following ways: In the `Rummage.Ecto` call: ```elixir - Rummage.Ecto.rummage(queryable, rummage, paginate: Rummage.Ecto.CustomHooks.KeysetPaginate) + Rummage.Ecto.rummage(queryable, rummage, + paginate: Rummage.Ecto.CustomHooks.KeysetPaginate) ``` OR diff --git a/lib/rummage_ecto/custom_hooks/simple_search.ex b/lib/rummage_ecto/custom_hooks/simple_search.ex index 4e8c798..4c74e92 100644 --- a/lib/rummage_ecto/custom_hooks/simple_search.ex +++ b/lib/rummage_ecto/custom_hooks/simple_search.ex @@ -4,8 +4,8 @@ defmodule Rummage.Ecto.CustomHooks.SimpleSearch do comes with `Rummage.Ecto`. This module provides a operations that can add searching functionality to - a pipeline of `Ecto` queries. This module works by taking fields, and `search_type`, - `search_term` and `assoc` associated with those `fields`. + a pipeline of `Ecto` queries. This module works by taking fields, and + `search_type`, `search_term` and `assoc` associated with those `fields`. This module doesn't support associations and hence is a simple alternative to Rummage's default search hook. @@ -30,17 +30,20 @@ defmodule Rummage.Ecto.CustomHooks.SimpleSearch do * `search_type`: Determines the kind of search to perform. If `:eq`, it expects the `field_name`'s value to be equal to `search_term`, If `lt`, it expects it to be less than `search_term`. - To see all the `search_type`s, check `Rummage.Ecto.Services.BuildSearchQuery` - * `search_expr`: This is optional. Defaults to `:where`. This is the way current + To see all the `search_type`s, check + `Rummage.Ecto.Services.BuildSearchQuery` + * `search_expr`: This is optional. Defaults to `:where`. This is the way the search expression is appended to the existing query. - To see all the `search_expr`s, check `Rummage.Ecto.Services.BuildSearchQuery` + To see all the `search_expr`s, check + `Rummage.Ecto.Services.BuildSearchQuery` For example, if we want to search products with `available` = `true`, we would do the following: ```elixir - Rummage.Ecto.CustomHooks.SimpleSearch.run(Product, %{available: %{search_type: :eq, + Rummage.Ecto.CustomHooks.SimpleSearch.run(Product, %{available: + %{search_type: :eq, search_term: true}} ``` @@ -61,7 +64,6 @@ defmodule Rummage.Ecto.CustomHooks.SimpleSearch do ____________________________________________________________________________ - # ASSUMPTIONS/NOTES: * This Hook assumes that the searched field is a part of the schema passed @@ -69,35 +71,9 @@ defmodule Rummage.Ecto.CustomHooks.SimpleSearch do * This Hook has the default `search_type` of `:eq`. * This Hook has the default `search_expr` of `:where`. - This module can be used by overriding the default module. This can be done - in the following ways: - - In the `Rummage.Ecto` call: - ```elixir - Rummage.Ecto.rummage(queryable, rummage, search: Rummage.Ecto.CustomHooks.SimpleSearch) - or - MySchema.rummage(rummage, search: Rummage.Ecto.CustomHooks.SimpleSearch) - ``` - - OR - - Globally for all models in `config.exs`: - ```elixir - config :my_app, - Rummage.Ecto, - search: Rummage.Ecto.CustomHooks.SimpleSearch - ``` - - OR - - When `using` Rummage.Ecto with an `Ecto.Schema`: - ```elixir - defmodule MySchema do - use Rummage.Ecto, repo: SomeRepo, - search: Rummage.Ecto.CustomHooks.SimpleSearch - end + ____________________________________________________________________________ - ## Usage: + # USAGE: For a regular search: @@ -107,7 +83,8 @@ defmodule Rummage.Ecto.CustomHooks.SimpleSearch do ```elixir alias Rummage.Ecto.CustomHooks.SimpleSearch - searched_queryable = SimpleSearch.run(Parent, %{field_1: %{search_type: :like, search_term: "field_!"}}}) + searched_queryable = SimpleSearch.run(Parent, + %{field_1: %{search_type: :like, search_term: "field_!"}}}) ``` @@ -121,34 +98,44 @@ defmodule Rummage.Ecto.CustomHooks.SimpleSearch do ```elixir alias Rummage.Ecto.CustomHooks.SimpleSearch - searched_queryable = SimpleSearch.run(Parent, %{field_1: %{ search_type: "ilike", search_term: "field_!"}}}) + searched_queryable = SimpleSearch.run(Parent, + %{field_1: %{ search_type: "ilike", search_term: "field_!"}}}) ``` - There are many other `search_types`. Check out `Rummage.Ecto.Services.BuildSearchQuery` docs - to explore more `search_types` + There are many other `search_types`. Check out + `Rummage.Ecto.Services.BuildSearchQuery` docs to explore more `search_types`. - This module can be overridden with a custom module while using `Rummage.Ecto` - in `Ecto` struct module: + This module can be used by overriding the default module. This can be done + in the following ways: - In the `Ecto` module: + In the `Rummage.Ecto` call: ```elixir - Rummage.Ecto.rummage(queryable, rummage, search: CustomHook) + Rummage.Ecto.rummage(queryable, rummage, + search: Rummage.Ecto.CustomHooks.SimpleSearch) + + or + + MySchema.rummage(rummage, search: Rummage.Ecto.CustomHooks.SimpleSearch) ``` OR Globally for all models in `config.exs`: ```elixir - config :rummage_ecto, + config :my_app, Rummage.Ecto, - .search: CustomHook + search: Rummage.Ecto.CustomHooks.SimpleSearch ``` - The `CustomHook` must use `Rummage.Ecto.Hook`. For examples of `CustomHook`, - check out some `custom_hooks` that are shipped with `Rummage.Ecto`: - `Rummage.Ecto.CustomHooks.SimpleSearch`, `Rummage.Ecto.CustomHooks.SimpleSort`, - Rummage.Ecto.CustomHooks.SimplePaginate + OR + + When `using` Rummage.Ecto with an `Ecto.Schema`: + ```elixir + defmodule MySchema do + use Rummage.Ecto, repo: SomeRepo, + search: Rummage.Ecto.CustomHooks.SimpleSearch + end """ use Rummage.Ecto.Hook From 9510f496beaca697f5cb72b3511c9b300d8457f1 Mon Sep 17 00:00:00 2001 From: Adi Date: Tue, 9 Jan 2018 13:38:33 -0500 Subject: [PATCH 59/64] Improve test coverage for KeysetPaginate --- lib/rummage_ecto/custom_hooks/keyset_paginate.ex | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/lib/rummage_ecto/custom_hooks/keyset_paginate.ex b/lib/rummage_ecto/custom_hooks/keyset_paginate.ex index 717dc27..c4242fb 100644 --- a/lib/rummage_ecto/custom_hooks/keyset_paginate.ex +++ b/lib/rummage_ecto/custom_hooks/keyset_paginate.ex @@ -358,14 +358,8 @@ defmodule Rummage.Ecto.CustomHooks.KeysetPaginate do end # This function gets count of a query and repo passed. - # When primary key passed is nil, it just gets all the elements - # and counts them, but when a primary key is passed it just counts + # A primary key must be passed and it just counts # the distinct primary keys - defp get_count(query, repo, nil) do - repo - |> apply(:all, [distinct(query, :true)]) - |> Enum.count() - end defp get_count(query, repo, pk) do query = select(query, [s], count(field(s, ^pk), :distinct)) hd(apply(repo, :all, [query])) From 009ef97d21579a20b55d219b8ee2dd476e71b1ca Mon Sep 17 00:00:00 2001 From: Adi Date: Tue, 9 Jan 2018 13:51:41 -0500 Subject: [PATCH 60/64] Update docs to fix line width --- lib/rummage_ecto/hooks/search.ex | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/lib/rummage_ecto/hooks/search.ex b/lib/rummage_ecto/hooks/search.ex index 320b233..450736d 100644 --- a/lib/rummage_ecto/hooks/search.ex +++ b/lib/rummage_ecto/hooks/search.ex @@ -28,10 +28,12 @@ defmodule Rummage.Ecto.Hooks.Search do * `search_type`: Determines the kind of search to perform. If `:eq`, it expects the `field_name`'s value to be equal to `search_term`, If `lt`, it expects it to be less than `search_term`. - To see all the `search_type`s, check `Rummage.Ecto.Services.BuildSearchQuery` + To see all the `search_type`s, check + `Rummage.Ecto.Services.BuildSearchQuery` * `search_expr`: This is optional. Defaults to `:where`. This is the way current search expression is appended to the existing query. - To see all the `search_expr`s, check `Rummage.Ecto.Services.BuildSearchQuery` + To see all the `search_expr`s, check + `Rummage.Ecto.Services.BuildSearchQuery` For example, if we want to search products with `available` = `true`, we would @@ -132,8 +134,8 @@ defmodule Rummage.Ecto.Hooks.Search do ``` - There are many other `search_types`. Check out `Rummage.Ecto.Services.BuildSearchQuery` docs - to explore more `search_types` + There are many other `search_types`. Check out + `Rummage.Ecto.Services.BuildSearchQuery` docs to explore more `search_types` This module can be overridden with a custom module while using `Rummage.Ecto` in `Ecto` struct module: From d41cdf26cc5a1a14410af55a9c9a1c099e61e7d8 Mon Sep 17 00:00:00 2001 From: Adi Date: Tue, 9 Jan 2018 13:59:01 -0500 Subject: [PATCH 61/64] Update Sort hook: - Add docs - Update docs with fields - Update doctests - Add help docs with about, assoc and usage --- lib/rummage_ecto/hooks/sort.ex | 77 ++++++++++++++++++++++++++++++++-- 1 file changed, 73 insertions(+), 4 deletions(-) diff --git a/lib/rummage_ecto/hooks/sort.ex b/lib/rummage_ecto/hooks/sort.ex index f8f35b3..12cf352 100644 --- a/lib/rummage_ecto/hooks/sort.ex +++ b/lib/rummage_ecto/hooks/sort.ex @@ -1,7 +1,5 @@ defmodule Rummage.Ecto.Hooks.Sort do @moduledoc """ - TODO: Explain how to use `assoc` better - `Rummage.Ecto.Hooks.Sort` is the default sort hook that comes with `Rummage.Ecto`. @@ -11,11 +9,81 @@ defmodule Rummage.Ecto.Hooks.Sort do which is a keyword list of assocations associated with those `fields`. NOTE: This module doesn't return a list of entries, but a `Ecto.Query.t`. + This module `uses` `Rummage.Ecto.Hook`. + _____________________________________________________________________________ - This module `uses` `Rummage.Ecto.Hook`. + # ABOUT: + + ## Arguments: + + This Hook expects a `queryable` (an `Ecto.Queryable`) and + `sort_params` (a `Map`). The map should be in the format: + `%{field: :field_name, assoc: [], order: :asc}` + + Details: + + * `field`: The field name (atom) to sorted by. + * `assoc`: List of associations in the sort. + * `order`: Specifies the type of order `asc` or `desc`. + * `ci` : Case Insensitivity. Defaults to `false` + + + For example, if we want to sort products with descending `price`, we would + do the following: + + ```elixir + Rummage.Ecto.Hooks.Sort.run(Product, %{field: :price, + assoc: [], order: :desc}) + ``` + + ## Assoications: + + Assocaitions can be given to this module's run function as a key corresponding + to params associated with a field. For example, if we want to sort products + that belong to a category by ascending category_name, we would do the + following: + + ```elixir + params = %{field: :category_name, assoc: [inner: :category], + order: :asc} + + Rummage.Ecto.Hooks.Sort.run(Product, params) + ``` + + The above operation will return an `Ecto.Query.t` struct which represents + a query equivalent to: + + ```elixir + from p in Product + |> join(:inner, :category) + |> order_by([p, c], {asc, c.category_name}) + ``` + + ____________________________________________________________________________ + + # ASSUMPTIONS/NOTES: + + * This Hook has the default `order` of `:asc`. + * This Hook has the default `assoc` of `[]`. + * This Hook assumes that the field passed is a field on the `Ecto.Schema` + that corresponds to the last association in the `assoc` list or the `Ecto.Schema` + that corresponds to the `from` in `queryable`, if `assoc` is an empty list. + + NOTE: It is adviced to not use multiple associated sorts in one operation + as `assoc` still has some minor bugs when used with multiple sorts. If you + need to use two sorts with associations, I would pipe the call to another + sort operation: + + ```elixir + Sort.run(queryable, params1} + |> Sort.run(%{field2: params2} + ``` + + ____________________________________________________________________________ + + # USAGE: - Usage: For a regular sort: This returns a `queryable` which upon running will give a list of `Parent`(s) @@ -62,6 +130,7 @@ defmodule Rummage.Ecto.Hooks.Sort do check out some `custom_hooks` that are shipped with `Rummage.Ecto`: `Rummage.Ecto.CustomHooks.SimpleSearch`, `Rummage.Ecto.CustomHooks.SimpleSort`, Rummage.Ecto.CustomHooks.SimplePaginate + """ use Rummage.Ecto.Hook From bebcfb7529f537989d40d90e688ecae67f7da4af Mon Sep 17 00:00:00 2001 From: Adi Date: Tue, 9 Jan 2018 13:59:35 -0500 Subject: [PATCH 62/64] Update SimpleSort Custom Hook: - Update functionality based on new hooks - Remove assoc - Update docs - Update doc tests --- lib/rummage_ecto/custom_hooks/simple_sort.ex | 303 ++++++++++--------- 1 file changed, 168 insertions(+), 135 deletions(-) diff --git a/lib/rummage_ecto/custom_hooks/simple_sort.ex b/lib/rummage_ecto/custom_hooks/simple_sort.ex index 4309056..7e66a09 100644 --- a/lib/rummage_ecto/custom_hooks/simple_sort.ex +++ b/lib/rummage_ecto/custom_hooks/simple_sort.ex @@ -1,218 +1,251 @@ defmodule Rummage.Ecto.CustomHooks.SimpleSort do @moduledoc """ - `Rummage.Ecto.CustomHooks.SimpleSort` is a custom sort hook that comes shipped - with `Rummage.Ecto`. + `Rummage.Ecto.CustomHooks.SimpleSort` is the default sort hook that comes with + `Rummage.Ecto`. + + This module provides a operations that can add sorting functionality to + a pipeline of `Ecto` queries. This module works by taking the `field` that should + be used to `order_by`, `order` which can be `asc` or `desc`. + + This module doesn't support associations and hence is a simple alternative + to Rummage's default search hook. + + NOTE: This module doesn't return a list of entries, but a `Ecto.Query.t`. + This module `uses` `Rummage.Ecto.Hook`. + + _____________________________________________________________________________ + + # ABOUT: + + ## Arguments: + + This Hook expects a `queryable` (an `Ecto.Queryable`) and + `sort_params` (a `Map`). The map should be in the format: + `%{field: :field_name, order: :asc}` + + Details: + + * `field`: The field name (atom) to sorted by. + * `order`: Specifies the type of order `asc` or `desc`. + * `ci` : Case Insensitivity. Defaults to `false` + + + For example, if we want to sort products with descending `price`, we would + do the following: + + ```elixir + Rummage.Ecto.CustomHooks.SimpleSort.run(Product, %{field: :price, + order: :desc}) + ``` + + ## Assoications: + + This module doesn't support assocations. + + ____________________________________________________________________________ + + # ASSUMPTIONS/NOTES: + + * This Hook has the default `order` of `:asc`. + * This Hook assumes that the searched field is a part of the schema passed + as the `queryable`. + + ____________________________________________________________________________ + + # USAGE: - Usage: For a regular sort: + This returns a `queryable` which upon running will give a list of `Parent`(s) + sorted by ascending `field_1` + ```elixir alias Rummage.Ecto.CustomHooks.SimpleSort - # This returns a queryable which upon running will give a list of `Parent`(s) - # sorted by ascending field_1 - sorted_queryable = SimpleSort.run(Parent, %{"sort" => "field_1.asc"}) + sorted_queryable = SimpleSort.run(Parent, %{field: :name, order: :asc}}) ``` For a case-insensitive sort: + This returns a `queryable` which upon running will give a list of `Parent`(s) + sorted by ascending case insensitive `field_1`. + + Keep in mind that `case_insensitive` can only be called for `text` fields + ```elixir alias Rummage.Ecto.CustomHooks.SimpleSort - # This returns a queryable which upon running will give a list of `Parent`(s) - # sorted by ascending case insensitive field_1 - # Keep in mind that case insensitive can only be called for text fields - sorted_queryable = SimpleSort.run(Parent, %{"sort" => "field_1.asc.ci"}) + sorted_queryable = SimpleSort.run(Parent, %{field: :name, order: :asc, ci: true}}) ``` - This module can be used by overriding the default sort module. This can be done - in the following ways: + + This module can be overridden with a custom module while using `Rummage.Ecto` + in `Ecto` struct module. In the `Ecto` module: ```elixir - Rummage.Ecto.rummage(queryable, rummage, sort: Rummage.Ecto.CustomHooks.SimpleSort) + Rummage.Ecto.rummage(queryable, rummage, sort: CustomHook) ``` OR - Globally for all models in `config.exs` (NOT Recommended): + Globally for all models in `config.exs`: ```elixir config :rummage_ecto, Rummage.Ecto, - .sort: Rummage.Ecto.CustomHooks.SimpleSort + .sort: CustomHook ``` + + The `CustomHook` must use `Rummage.Ecto.Hook`. For examples of `CustomHook`, + check out some `custom_hooks` that are shipped with `Rummage.Ecto`: + `Rummage.Ecto.CustomHooks.SimpleSearch`, `Rummage.Ecto.CustomHooks.SimpleSort`, + Rummage.Ecto.CustomHooks.SimplePaginate + """ + use Rummage.Ecto.Hook + import Ecto.Query - @behaviour Rummage.Ecto.Hook + @expected_keys ~w(field order)a + @err_msg "Error in params, No values given for keys: " @doc """ - Builds a sort `queryable` on top of the given `queryable` from the rummage parameters - from the given `rummage` struct. + This is the callback implementation of `Rummage.Ecto.Hook.run/2`. - ## Examples - When rummage `struct` passed doesn't have the key `"sort"`, it simply returns the - `queryable` itself: + Builds a sort `Ecto.Query.t` on top of the given `Ecto.Queryable` variable + using given `params`. - iex> alias Rummage.Ecto.CustomHooks.SimpleSort - iex> import Ecto.Query - iex> SimpleSort.run(Parent, %{}) - Parent + Besides an `Ecto.Query.t` an `Ecto.Schema` module can also be passed as it + implements `Ecto.Queryable` - When the `queryable` passed is not just a `struct`: + Params is a `Map` which is expected to have the keys `#{Enum.join(@expected_keys, ", ")}`. - iex> alias Rummage.Ecto.CustomHooks.SimpleSort - iex> import Ecto.Query - iex> queryable = from u in "parents" - #Ecto.Query - iex> SimpleSort.run(queryable, %{}) - #Ecto.Query + This funciton expects a `field` atom, `order` which can be `asc` or `desc`, + `ci` which is a boolean indicating the case-insensitivity. - When rummage `struct` passed has the key `"sort"`, but with a value of `{}`, `""` - or `[]` it simply returns the `queryable` itself: + ## Examples + When an empty map is passed as `params`: iex> alias Rummage.Ecto.CustomHooks.SimpleSort - iex> import Ecto.Query - iex> SimpleSort.run(Parent, %{"sort" => {}}) - Parent + iex> SimpleSort.run(Parent, %{}) + ** (RuntimeError) Error in params, No values given for keys: field, order - iex> alias Rummage.Ecto.CustomHooks.SimpleSort - iex> import Ecto.Query - iex> SimpleSort.run(Parent, %{"sort" => ""}) - Parent + When a non-empty map is passed as `params`, but with a missing key: iex> alias Rummage.Ecto.CustomHooks.SimpleSort - iex> import Ecto.Query - iex> SimpleSort.run(Parent, %{"sort" => []}) - Parent + iex> SimpleSort.run(Parent, %{field: :name}) + ** (RuntimeError) Error in params, No values given for keys: order - When rummage `struct` passed has the key `"sort"`, with `field` and `order` - it returns a sorted version of the `queryable` passed in as the argument: + When a valid map of params is passed with an `Ecto.Schema` module: iex> alias Rummage.Ecto.CustomHooks.SimpleSort - iex> import Ecto.Query - iex> rummage = %{"sort" => "field_1.asc"} - %{"sort" => "field_1.asc"} - iex> queryable = from u in "parents" - #Ecto.Query - iex> SimpleSort.run(queryable, rummage) - #Ecto.Query + iex> SimpleSort.run(Rummage.Ecto.Product, %{field: :name, order: :asc}) + #Ecto.Query + When the `queryable` passed is an `Ecto.Query` variable: iex> alias Rummage.Ecto.CustomHooks.SimpleSort iex> import Ecto.Query - iex> rummage = %{"sort" => "field_1.desc"} - %{"sort" => "field_1.desc"} - iex> queryable = from u in "parents" - #Ecto.Query - iex> SimpleSort.run(queryable, rummage) - #Ecto.Query + iex> queryable = from u in "products" + #Ecto.Query + iex> SimpleSort.run(queryable, %{field: :name, order: :asc}) + #Ecto.Query - When no `order` is specified, it returns the `queryable` itself: + When the `queryable` passed is an `Ecto.Query` variable, with `desc` order: iex> alias Rummage.Ecto.CustomHooks.SimpleSort iex> import Ecto.Query - iex> rummage = %{"sort" => "field_1"} - %{"sort" => "field_1"} - iex> queryable = from u in "parents" - #Ecto.Query - iex> SimpleSort.run(queryable, rummage) - #Ecto.Query + iex> queryable = from u in "products" + #Ecto.Query + iex> SimpleSort.run(queryable, %{field: :name, order: :desc}) + #Ecto.Query - When rummage `struct` passed has `case-insensitive` sort, it returns - a sorted version of the `queryable` with `case_insensitive` arguments: + When the `queryable` passed is an `Ecto.Query` variable, with `ci` true: iex> alias Rummage.Ecto.CustomHooks.SimpleSort iex> import Ecto.Query - iex> rummage = %{"sort" => "field_1.asc.ci"} - %{"sort" => "field_1.asc.ci"} - iex> queryable = from u in "parents" - #Ecto.Query - iex> SimpleSort.run(queryable, rummage) - #Ecto.Query + iex> queryable = from u in "products" + #Ecto.Query + iex> SimpleSort.run(queryable, %{field: :name, order: :asc, ci: true}) + #Ecto.Query - iex> alias Rummage.Ecto.CustomHooks.SimpleSort - iex> import Ecto.Query - iex> rummage = %{"sort" => "field_1.desc.ci"} - %{"sort" => "field_1.desc.ci"} - iex> queryable = from u in "parents" - #Ecto.Query - iex> SimpleSort.run(queryable, rummage) - #Ecto.Query """ - @spec run(Ecto.Query.t, map) :: {Ecto.Query.t, map} - def run(queryable, rummage) do - sort_params = Map.get(rummage, "sort") - - case sort_params do - a when a in [nil, [], {}, ""] -> queryable - _ -> - case Regex.match?(~r/\w.ci+$/, sort_params) do - true -> - sort_params = sort_params - |> String.split(".") - |> Enum.drop(-1) - |> Enum.join(".") - - handle_ci_sort(queryable, sort_params) - _ -> handle_sort(queryable, sort_params) - end - end + @spec run(Ecto.Query.t(), map()) :: Ecto.Query.t() + def run(queryable, sort_params) do + :ok = validate_params(sort_params) + + handle_sort(queryable, sort_params) end - @doc """ - Implementation of `before_hook` for `Rummage.Ecto.CustomHooks.SimpleSort`. This just returns back `rummage` at this point. - It doesn't matter what `queryable` or `opts` are, it just returns back `rummage`. + # Helper function which handles addition of paginated query on top of + # the sent queryable variable + defp handle_sort(queryable, sort_params) do + order = Map.get(sort_params, :order) + field = Map.get(sort_params, :field) + ci = Map.get(sort_params, :ci, false) - ## Examples - iex> alias Rummage.Ecto.CustomHooks.SimpleSort - iex> SimpleSort.before_hook(Parent, %{}, %{}) - %{} - """ - @spec before_hook(Ecto.Query.t, map, map) :: map - def before_hook(_queryable, rummage, _opts), do: rummage + handle_ordering(from(e in subquery(queryable)), field, order, ci) + end + # This is a helper macro to get case_insensitive query using fragments defmacrop case_insensitive(field) do quote do fragment("lower(?)", unquote(field)) end end - defp handle_sort(queryable, sort_params), do: queryable |> order_by(^consolidate_order_params(sort_params)) + # Helper function that handles adding order_by to a query based on order type + # case insensitivity and field + defp handle_ordering(queryable, field, order, ci) do + order_by_assoc(queryable, order, field, ci) + end - defp handle_ci_sort(queryable, sort_params) do - order_param = sort_params - |> consolidate_order_params - |> Enum.at(0) + defp order_by_assoc(queryable, order_type, field, false) do + order_by(queryable, [p0, ..., p2], [{^order_type, field(p2, ^field)}]) + end - queryable - |> order_by([{^elem(order_param, 0), - case_insensitive(^elem(order_param, 1))}]) + defp order_by_assoc(queryable, order_type, field, true) do + order_by(queryable, [p0, ..., p2], + [{^order_type, case_insensitive(field(p2, ^field))}]) end - defp consolidate_order_params(sort_params) do - case Regex.match?(~r/\w.asc+$/, sort_params) - or Regex.match?(~r/\w.desc+$/, sort_params) - do - true -> add_order_params([], sort_params) - _ -> [] + # Helper function that validates the list of params based on + # @expected_keys list + defp validate_params(params) do + key_validations = Enum.map(@expected_keys, &Map.fetch(params, &1)) + + case Enum.filter(key_validations, & &1 == :error) do + [] -> :ok + _ -> raise @err_msg <> missing_keys(key_validations) end end - defp add_order_params(order_params, unparsed_field) do - parsed_field = unparsed_field - |> String.split(".") - |> Enum.drop(-1) - |> Enum.join(".") - |> String.to_atom + # Helper function used to build error message using missing keys + defp missing_keys(key_validations) do + key_validations + |> Enum.with_index() + |> Enum.filter(fn {v, _i} -> v == :error end) + |> Enum.map(fn {_v, i} -> Enum.at(@expected_keys, i) end) + |> Enum.map(&to_string/1) + |> Enum.join(", ") + end + + @doc """ + Callback implementation for `Rummage.Ecto.Hook.format_params/2`. - order_type = unparsed_field - |> String.split(".") - |> Enum.at(-1) - |> String.to_atom + This function ensures that params for each field have keys `assoc`, `order1 + which are essential for running this hook module. - Keyword.put(order_params, order_type, parsed_field) + ## Examples + iex> alias Rummage.Ecto.CustomHooks.SimpleSort + iex> SimpleSort.format_params(Parent, %{}, []) + %{order: :asc} + """ + @spec format_params(Ecto.Query.t(), map(), keyword()) :: map() + def format_params(_queryable, sort_params, _opts) do + sort_params + |> Map.put_new(:order, :asc) end end From f1f4ef30e9582e214d8843ceabb3ad7ab36cebf8 Mon Sep 17 00:00:00 2001 From: Adi Date: Tue, 9 Jan 2018 14:03:50 -0500 Subject: [PATCH 63/64] Update Paginate docs: - Add about and assumptions --- lib/rummage_ecto/hooks/paginate.ex | 35 ++++++++++++++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/lib/rummage_ecto/hooks/paginate.ex b/lib/rummage_ecto/hooks/paginate.ex index 5b537bd..4c3aa0e 100644 --- a/lib/rummage_ecto/hooks/paginate.ex +++ b/lib/rummage_ecto/hooks/paginate.ex @@ -10,11 +10,42 @@ defmodule Rummage.Ecto.Hooks.Paginate do NOTE: This module doesn't return a list of entries, but a `Ecto.Query.t`. + This module `uses` `Rummage.Ecto.Hook`. + _____________________________________________________________________________ - This module `uses` `Rummage.Ecto.Hook`. + # ABOUT: + + ## Arguments: + + This Hook expects a `queryable` (an `Ecto.Queryable`) and + `paginate_params` (a `Map`). The map should be in the format: + `%{per_page: 10, page: 1}` + + Details: + + * `per_page`: Specifies the entries in each page. + * `page`: Specifies the `page` number. + + + For example, if we want to paginate products, we would + do the following: + + ```elixir + Rummage.Ecto.Hooks.Paginate.run(Product, %{per_page: 10, page: 1}) + ``` + + _____________________________________________________________________________ + + # ASSUMPTIONS/NOTES: + + NONE: This Hook should work for all the `Schema` types. Whether the schema has + a primary_key or not, this should handle that. + + _____________________________________________________________________________ + + ## USAGE: - ## Usage: To add pagination to a `Ecto.Queryable`, simply do the following: ```ex From c99191a2b39eaf569fbca7a5bf16387bf8949be5 Mon Sep 17 00:00:00 2001 From: Adi Date: Tue, 9 Jan 2018 14:04:02 -0500 Subject: [PATCH 64/64] Fix syntax in docs --- lib/rummage_ecto/custom_hooks/keyset_paginate.ex | 2 +- lib/rummage_ecto/custom_hooks/simple_search.ex | 4 ++-- lib/rummage_ecto/hooks/search.ex | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/rummage_ecto/custom_hooks/keyset_paginate.ex b/lib/rummage_ecto/custom_hooks/keyset_paginate.ex index c4242fb..72a7cd6 100644 --- a/lib/rummage_ecto/custom_hooks/keyset_paginate.ex +++ b/lib/rummage_ecto/custom_hooks/keyset_paginate.ex @@ -37,7 +37,7 @@ defmodule Rummage.Ecto.CustomHooks.KeysetPaginate do ```elixir Rummage.Ecto.CustomHooks.KeysetPaginate.run(Product, - %{per_page: 10, page: 1, last_seen_pk: 10, pk: :id} + %{per_page: 10, page: 1, last_seen_pk: 10, pk: :id}) ``` ## When to Use KeysetPaginate? diff --git a/lib/rummage_ecto/custom_hooks/simple_search.ex b/lib/rummage_ecto/custom_hooks/simple_search.ex index 4c74e92..9834d88 100644 --- a/lib/rummage_ecto/custom_hooks/simple_search.ex +++ b/lib/rummage_ecto/custom_hooks/simple_search.ex @@ -44,7 +44,7 @@ defmodule Rummage.Ecto.CustomHooks.SimpleSearch do ```elixir Rummage.Ecto.CustomHooks.SimpleSearch.run(Product, %{available: %{search_type: :eq, - search_term: true}} + search_term: true}}) ``` This can be used for a search with multiple fields as well. Say, we want to @@ -55,7 +55,7 @@ defmodule Rummage.Ecto.CustomHooks.SimpleSearch do %{available: %{search_type: :eq, search_term: true}, %{price: %{search_type: :lt, - search_term: 10.0}} + search_term: 10.0}}) ``` ## Assoications: diff --git a/lib/rummage_ecto/hooks/search.ex b/lib/rummage_ecto/hooks/search.ex index 450736d..b582e60 100644 --- a/lib/rummage_ecto/hooks/search.ex +++ b/lib/rummage_ecto/hooks/search.ex @@ -42,7 +42,7 @@ defmodule Rummage.Ecto.Hooks.Search do ```elixir Rummage.Ecto.Hooks.Search.run(Product, %{available: %{assoc: [], search_type: :eq, - search_term: true}} + search_term: true}}) ``` This can be used for a search with multiple fields as well. Say, we want to @@ -55,7 +55,7 @@ defmodule Rummage.Ecto.Hooks.Search do search_term: true}, %{price: %{assoc: [], search_type: :lt, - search_term: 10.0}} + search_term: 10.0}}) ``` ## Assoications: