diff --git a/.github/workflows/elixir.yml b/.github/workflows/elixir.yml index 8020d50..c92c13f 100644 --- a/.github/workflows/elixir.yml +++ b/.github/workflows/elixir.yml @@ -18,9 +18,19 @@ jobs: otp: ["23", "22"] elixir: ["1.10.0"] ash: ["master", "1.12", "1.13"] + pg_version: ["9.5", "9.6", "11"] env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} ASH_VERSION: ${{matrix.ash}} + services: + pg: + image: postgres:${{ matrix.pg_version }} + env: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: postgres + options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 + ports: ["5432:5432"] steps: - run: sudo apt-get install --yes erlang-dev - uses: actions/checkout@v2 @@ -41,6 +51,8 @@ jobs: key: otp-${{matrix.otp}}-elixir-${{matrix.elixir}}-build-2-${{ hashFiles(format('{0}{1}', github.workspace, '/mix.lock')) }} restore-keys: otp-${{matrix.otp}}-elixir-${{matrix.elixir}}-build-2 - run: mix deps.get + - run: MIX_ENV=test mix ecto.create + - run: MIX_ENV=test mix ecto.migrate - run: mix check --except dialyzer if: startsWith(github.ref, 'refs/tags/v') - run: mix check @@ -53,8 +65,18 @@ jobs: matrix: otp: ["23"] elixir: ["1.10.0"] + pg_version: ["11"] env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + services: + pg: + image: postgres:${{ matrix.pg_version }} + env: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: postgres + options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 + ports: ["5432:5432"] steps: - run: sudo apt-get install --yes erlang-dev - uses: actions/checkout@v2 @@ -69,6 +91,8 @@ jobs: key: otp-${{matrix.otp}}-elixir-${{matrix.elixir}}-deps-2-${{ hashFiles(format('{0}{1}', github.workspace, '/mix.lock')) }} restore-keys: otp-${{matrix.otp}}-elixir-${{matrix.elixir}}-deps-2- - run: mix deps.get + - run: MIX_ENV=test mix ecto.create + - run: MIX_ENV=test mix ecto.migrate - run: mix coveralls.github release: runs-on: ubuntu-latest diff --git a/CHANGELOG.md b/CHANGELOG.md index 87b5096..727d86d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,249 +7,173 @@ See [Conventional Commits](Https://conventionalcommits.org) for commit guideline ## [v0.19.0](https://github.com/ash-project/ash_postgres/compare/v0.18.0...v0.19.0) (2020-09-02) - - - ### Features: -* support inner joins when possible (#15) +- support inner joins when possible (#15) ### Bug Fixes: -* better support for aggregates/calculations when delegating +- better support for aggregates/calculations when delegating -* don't fail w/ no extensions configured +- don't fail w/ no extensions configured ## [v0.18.0](https://github.com/ash-project/ash_postgres/compare/v0.17.0...v0.18.0) (2020-08-26) - - - ### Features: -* update to ash 1.11 (#13) +- update to ash 1.11 (#13) -* support Ash v1.10 (#12) +- support Ash v1.10 (#12) -* support latest ash +- support latest ash -* update to latest ash +- update to latest ash ## [v0.17.0](https://github.com/ash-project/ash_postgres/compare/v0.16.1...v0.17.0) (2020-08-26) - - - ### Features: -* update to ash 1.11 (#13) +- update to ash 1.11 (#13) -* support Ash v1.10 (#12) +- support Ash v1.10 (#12) -* support latest ash +- support latest ash -* update to latest ash +- update to latest ash ## [v0.16.1](https://github.com/ash-project/ash_postgres/compare/v0.16.0...v0.16.1) (2020-08-19) - - - ### Bug Fixes: -* fix compile/dialyzer issues +- fix compile/dialyzer issues ## [v0.16.0](https://github.com/ash-project/ash_postgres/compare/v0.15.0...v0.16.0) (2020-08-19) - - - ### Features: -* update to latest ash +- update to latest ash -* update to latest version of ash +- update to latest version of ash ## [v0.15.0](https://github.com/ash-project/ash_postgres/compare/v0.14.0...v0.15.0) (2020-08-18) - - - ### Features: -* update to latest version of ash +- update to latest version of ash ## [v0.14.0](https://github.com/ash-project/ash_postgres/compare/v0.13.0...v0.14.0) (2020-08-17) - - - ### Features: -* support ash 1.7 +- support ash 1.7 -* support named aggregates +- support named aggregates ## [v0.13.0](https://github.com/ash-project/ash_postgres/compare/v0.12.1...v0.13.0) (2020-07-25) - - - ### Features: -* update to latest ash +- update to latest ash -* support latest ash +- support latest ash ## [v0.12.1](https://github.com/ash-project/ash_postgres/compare/v0.12.0...v0.12.1) (2020-07-24) - - - ### Bug Fixes: -* add can? for `:aggregate` +- add can? for `:aggregate` ## [v0.12.0](https://github.com/ash-project/ash_postgres/compare/0.11.2...v0.12.0) (2020-07-24) - - - ### Features: -* update to latest ash +- update to latest ash ## [v0.11.2](https://github.com/ash-project/ash_postgres/compare/0.11.1...v0.11.2) (2020-07-23) - - - ### Bug Fixes: -* typespecs, errant IO.inspect - ## [v0.11.1](https://github.com/ash-project/ash_postgres/compare/0.11.0...v0.11.1) (2020-07-23) - - - ### Bug Fixes: -* typespecs, errant IO.inspect - ## [v0.11.0](https://github.com/ash-project/ash_postgres/compare/0.10.0...v0.11.0) (2020-07-23) - - - ### Features: -* support ash 13.0 aggregates +- support ash 13.0 aggregates ## [v0.10.0](https://github.com/ash-project/ash_postgres/compare/0.9.0...v0.10.0) (2020-07-15) - - - ### Features: -* update to latest ash +- update to latest ash ## [v0.9.0](https://github.com/ash-project/ash_postgres/compare/0.8.0...v0.9.0) (2020-07-13) - - - ### Features: -* update to latest ash +- update to latest ash ## [v0.8.0](https://github.com/ash-project/ash_postgres/compare/0.7.0...v0.8.0) (2020-07-09) - - - ### Features: -* update to latest ash +- update to latest ash ## [v0.7.0](https://github.com/ash-project/ash_postgres/compare/0.6.0...v0.7.0) (2020-07-09) - - - ### Features: -* update to latest ash +- update to latest ash -* update to latest ash, add docs +- update to latest ash, add docs -* update to ash 0.9.1 for transactions +- update to ash 0.9.1 for transactions ## [v0.6.0](https://github.com/ash-project/ash_postgres/compare/0.5.0...v0.6.0) (2020-06-29) - - - ### Features: -* update to latest ash +- update to latest ash ## [v0.5.0](https://github.com/ash-project/ash_postgres/compare/0.4.0...v0.5.0) (2020-06-29) - - - ### Features: -* upgrade to latest ash +- upgrade to latest ash ## [v0.4.0](https://github.com/ash-project/ash_postgres/compare/0.3.0...v0.4.0) (2020-06-27) - - - ### Features: -* update to latest ash +- update to latest ash ## [v0.3.0](https://github.com/ash-project/ash_postgres/compare/0.2.1...v0.3.0) (2020-06-19) - - - ### Features: -* New filter style (#10) +- New filter style (#10) ## [v0.2.1](https://github.com/ash-project/ash_postgres/compare/0.2.0...v0.2.1) (2020-06-15) - - - ### Bug Fixes: -* update .formatter.exs +- update .formatter.exs ## [v0.2.0](https://github.com/ash-project/ash_postgres/compare/0.1.4...v0.2.0) (2020-06-14) - - - ### Features: -* use the new DSL builder for config (#7) +- use the new DSL builder for config (#7) ## [v0.1.4](https://github.com/ash-project/ash_postgres/compare/0.1.3...v0.1.4) (2020-06-05) - - - ### Bug Fixes: -* update ash version dependency +- update ash version dependency -* account for removal of name +- account for removal of name ## [v0.1.3](https://github.com/ash-project/ash_postgres/compare/0.1.2...v0.1.3) (2020-06-03) diff --git a/README.md b/README.md index ab23aec..8ba9caa 100644 --- a/README.md +++ b/README.md @@ -4,3 +4,5 @@ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![Coverage Status](https://coveralls.io/repos/github/ash-project/ash_postgres/badge.svg?branch=master)](https://coveralls.io/github/ash-project/ash_postgres?branch=master) [![Hex version badge](https://img.shields.io/hexpm/v/ash_postgres.svg)](https://hex.pm/packages/ash_postgres) + +The documentation for ash_postgres is available on [hexdocs](https://hexdocs.pm/ash_postgres/AshPostgres.html) diff --git a/config/config.exs b/config/config.exs index fe9d587..bfeb87a 100644 --- a/config/config.exs +++ b/config/config.exs @@ -13,3 +13,23 @@ if Mix.env() == :dev do manage_readme_version: "README.md", version_tag_prefix: "v" end + +if Mix.env() == :test do + # Configure your database + # + + config :ash_postgres, AshPostgres.TestRepo, + username: "postgres", + database: "postgres", + hostname: "localhost", + pool: Ecto.Adapters.SQL.Sandbox + + # sobelow_skip ["Config.Secrets"] + config :ash_postgres, AshPostgres.TestRepo, password: "postgres" + + config :ash_postgres, ecto_repos: [AshPostgres.TestRepo] + + config :ash_postgres, AshPostgres.TestRepo, migration_primary_key: [name: :id, type: :binary_id] + + config :logger, level: :warn +end diff --git a/lib/data_layer.ex b/lib/data_layer.ex index 1fa33d1..591f838 100644 --- a/lib/data_layer.ex +++ b/lib/data_layer.ex @@ -2,7 +2,17 @@ defmodule AshPostgres.DataLayer do @moduledoc """ A postgres data layer that levereges Ecto's postgres capabilities. - To use this data layer, you need to define an `Ecto.Repo`. Ash adds some + AshPostgres supports all capabilities of an Ash data layer, and it will + most likely stay that way, as postgres is the primary target/most maintained + data layer. + + Custom Predicates: + + * AshPostgres.Predicates.Trigram + + ### Usage + + To use this data layer, you need to define an `AshPostgres.Repo`. Ash adds some functionality on top of ecto repos, so you'll want to use `AshPostgres.Repo` Then, configure your resource like so: @@ -24,7 +34,7 @@ defmodule AshPostgres.DataLayer do type: {:custom, AshPostgres.DataLayer, :validate_repo, []}, required: true, doc: - "The repo that will be used to fetch your data. See the `Ecto.Repo` documentation for more" + "The repo that will be used to fetch your data. See the `AshPostgres.Repo` documentation for more" ], table: [ type: :string, diff --git a/lib/repo.ex b/lib/repo.ex index 8903391..13fbf34 100644 --- a/lib/repo.ex +++ b/lib/repo.ex @@ -7,6 +7,18 @@ defmodule AshPostgres.Repo do You can use `Ecto.Repo`'s `init/2` to configure your repo like normal, but instead of returning `{:ok, config}`, use `super(config)` to pass the configuration to the `AshPostgres.Repo` implementation. + + Currently the only additional configuration supported is `installed_extensions`, + and the only extension that ash_postgres reacts to is `"pg_trgm"`. If this extension + is installed, then the `AshPostgres.Predicates.Trigram` custom predicate will be + available. + + + ``` + def installed_extensions() do + ["pg_trgm"] + end + ``` """ @doc "Use this to inform the data layer about what extensions are installed" diff --git a/mix.exs b/mix.exs index a85898a..384b5ea 100644 --- a/mix.exs +++ b/mix.exs @@ -17,6 +17,7 @@ defmodule AshPostgres.MixProject do deps: deps(), description: @description, test_coverage: [tool: ExCoveralls], + elixirc_paths: elixirc_paths(Mix.env()), preferred_cli_env: [ coveralls: :test, "coveralls.github": :test @@ -32,6 +33,9 @@ defmodule AshPostgres.MixProject do ] end + defp elixirc_paths(:test), do: ["lib", "test/support"] + defp elixirc_paths(_), do: ["lib"] + defp package do [ name: :ash_postgres, @@ -83,7 +87,7 @@ defmodule AshPostgres.MixProject do defp aliases do [ - sobelow: "sobelow --skip", + sobelow: "sobelow --skip -i Config.Secrets", credo: "credo --strict", "ash.formatter": "ash.formatter --extensions AshPostgres.DataLayer" ] diff --git a/priv/test_repo/migrations/20200903065656_add_posts.exs b/priv/test_repo/migrations/20200903065656_add_posts.exs new file mode 100644 index 0000000..3b0e74e --- /dev/null +++ b/priv/test_repo/migrations/20200903065656_add_posts.exs @@ -0,0 +1,11 @@ +defmodule AshPostgres.TestRepo.Migrations.AddPosts do + use Ecto.Migration + + def change do + create table(:posts) do + add(:title, :string) + add(:score, :integer) + add(:public, :boolean) + end + end +end diff --git a/priv/test_repo/migrations/20200903065659_add_comments.exs b/priv/test_repo/migrations/20200903065659_add_comments.exs new file mode 100644 index 0000000..ca0354d --- /dev/null +++ b/priv/test_repo/migrations/20200903065659_add_comments.exs @@ -0,0 +1,10 @@ +defmodule AshPostgres.TestRepo.Migrations.AddComments do + use Ecto.Migration + + def change do + create table(:comments) do + add(:title, :string) + add(:post_id, references(:posts)) + end + end +end diff --git a/test/ash_postgres_test.exs b/test/ash_postgres_test.exs index ab88ed4..10303a2 100644 --- a/test/ash_postgres_test.exs +++ b/test/ash_postgres_test.exs @@ -1,8 +1,3 @@ defmodule AshPostgresTest do use ExUnit.Case - doctest AshPostgres - - test "works" do - assert true - end end diff --git a/test/filter_test.exs b/test/filter_test.exs new file mode 100644 index 0000000..8372de4 --- /dev/null +++ b/test/filter_test.exs @@ -0,0 +1,273 @@ +defmodule AshPostgres.FilterTest do + use AshPostgres.RepoCase + + defmodule Post do + use Ash.Resource, + data_layer: AshPostgres.DataLayer + + postgres do + table "posts" + repo AshPostgres.TestRepo + end + + actions do + read(:read) + create(:create) + end + + attributes do + attribute(:id, :uuid, primary_key?: true, default: &Ecto.UUID.generate/0) + attribute(:title, :string) + attribute(:score, :integer) + attribute(:public, :boolean) + end + + relationships do + has_many(:comments, AshPostgres.FilterTest.Comment, destination_field: :post_id) + end + end + + defmodule Comment do + use Ash.Resource, + data_layer: AshPostgres.DataLayer + + postgres do + table "comments" + repo AshPostgres.TestRepo + end + + actions do + read(:read) + create(:create) + end + + attributes do + attribute(:id, :uuid, primary_key?: true, default: &Ecto.UUID.generate/0) + attribute(:title, :string) + end + + relationships do + belongs_to(:post, Post) + end + end + + defmodule Api do + use Ash.Api + + resources do + resource(Post) + resource(Comment) + end + end + + describe "with no filter applied" do + test "with no data" do + assert [] = Api.read!(Post) + end + + test "with data" do + Post + |> Ash.Changeset.new(%{title: "title"}) + |> Api.create!() + + assert [%Post{title: "title"}] = Api.read!(Post) + end + end + + describe "with a simple filter applied" do + test "with no data" do + results = + Post + |> Ash.Query.filter(title: "title") + |> Api.read!() + + assert [] = results + end + + test "with data that matches" do + Post + |> Ash.Changeset.new(%{title: "title"}) + |> Api.create!() + + results = + Post + |> Ash.Query.filter(title: "title") + |> Api.read!() + + assert [%Post{title: "title"}] = results + end + + test "with some data that matches and some data that doesnt" do + Post + |> Ash.Changeset.new(%{title: "title"}) + |> Api.create!() + + results = + Post + |> Ash.Query.filter(title: "no_title") + |> Api.read!() + + assert [] = results + end + + test "with related data that doesn't match" do + post = + Post + |> Ash.Changeset.new(%{title: "title"}) + |> Api.create!() + + Comment + |> Ash.Changeset.new(%{title: "not match"}) + |> Ash.Changeset.replace_relationship(:post, post) + |> Api.create!() + + results = + Post + |> Ash.Query.filter(comments: [title: "match"]) + |> Api.read!() + + assert [] = results + end + + test "with related data that does match" do + post = + Post + |> Ash.Changeset.new(%{title: "title"}) + |> Api.create!() + + Comment + |> Ash.Changeset.new(%{title: "match"}) + |> Ash.Changeset.replace_relationship(:post, post) + |> Api.create!() + + results = + Post + |> Ash.Query.filter(comments: [title: "match"]) + |> Api.read!() + + assert [%Post{title: "title"}] = results + end + + test "with related data that does and doesn't match" do + post = + Post + |> Ash.Changeset.new(%{title: "title"}) + |> Api.create!() + + Comment + |> Ash.Changeset.new(%{title: "match"}) + |> Ash.Changeset.replace_relationship(:post, post) + |> Api.create!() + + Comment + |> Ash.Changeset.new(%{title: "not match"}) + |> Ash.Changeset.replace_relationship(:post, post) + |> Api.create!() + + results = + Post + |> Ash.Query.filter(comments: [title: "match"]) + |> Api.read!() + + assert [%Post{title: "title"}] = results + end + end + + describe "with a boolean filter applied" do + test "with no data" do + results = + Post + |> Ash.Query.filter(or: [[title: "title"], [score: 1]]) + |> Api.read!() + + assert [] = results + end + + test "with data that doesn't match" do + Post + |> Ash.Changeset.new(%{title: "no title", score: 2}) + |> Api.create!() + + results = + Post + |> Ash.Query.filter(or: [[title: "title"], [score: 1]]) + |> Api.read!() + + assert [] = results + end + + test "with data that matches both conditions" do + Post + |> Ash.Changeset.new(%{title: "title", score: 0}) + |> Api.create!() + + Post + |> Ash.Changeset.new(%{score: 1, title: "nothing"}) + |> Api.create!() + + results = + Post + |> Ash.Query.filter(or: [[title: "title"], [score: 1]]) + |> Api.read!() + |> Enum.sort_by(& &1.score) + + assert [%Post{title: "title", score: 0}, %Post{title: "nothing", score: 1}] = results + end + + test "with data that matches one condition and data that matches nothing" do + Post + |> Ash.Changeset.new(%{title: "title", score: 0}) + |> Api.create!() + + Post + |> Ash.Changeset.new(%{score: 2, title: "nothing"}) + |> Api.create!() + + results = + Post + |> Ash.Query.filter(or: [[title: "title"], [score: 1]]) + |> Api.read!() + |> Enum.sort_by(& &1.score) + + assert [%Post{title: "title", score: 0}] = results + end + + test "with related data in an or statement that matches, while basic filter doesn't match" do + post = + Post + |> Ash.Changeset.new(%{title: "doesn't match"}) + |> Api.create!() + + Comment + |> Ash.Changeset.new(%{title: "match"}) + |> Ash.Changeset.replace_relationship(:post, post) + |> Api.create!() + + results = + Post + |> Ash.Query.filter(or: [[title: "match"], [comments: [title: "match"]]]) + |> Api.read!() + + assert [%Post{title: "doesn't match"}] = results + end + + test "with related data in an or statement that doesn't match, while basic filter does match" do + post = + Post + |> Ash.Changeset.new(%{title: "match"}) + |> Api.create!() + + Comment + |> Ash.Changeset.new(%{title: "doesn't match"}) + |> Ash.Changeset.replace_relationship(:post, post) + |> Api.create!() + + results = + Post + |> Ash.Query.filter(or: [[title: "match"], [comments: [title: "match"]]]) + |> Api.read!() + + assert [%Post{title: "match"}] = results + end + end +end diff --git a/test/support/repo_case.ex b/test/support/repo_case.ex new file mode 100644 index 0000000..f4b535c --- /dev/null +++ b/test/support/repo_case.ex @@ -0,0 +1,28 @@ +defmodule AshPostgres.RepoCase do + @moduledoc false + use ExUnit.CaseTemplate + + alias Ecto.Adapters.SQL.Sandbox + + using do + quote do + alias AshPostgres.TestRepo + + import Ecto + import Ecto.Query + import AshPostgres.RepoCase + + # and any other stuff + end + end + + setup tags do + :ok = Sandbox.checkout(AshPostgres.TestRepo) + + unless tags[:async] do + Sandbox.mode(AshPostgres.TestRepo, {:shared, self()}) + end + + :ok + end +end diff --git a/test/support/test_repo.ex b/test/support/test_repo.ex new file mode 100644 index 0000000..7a5a1f8 --- /dev/null +++ b/test/support/test_repo.ex @@ -0,0 +1,5 @@ +defmodule AshPostgres.TestRepo do + @moduledoc false + use AshPostgres.Repo, + otp_app: :ash_postgres +end diff --git a/test/test_helper.exs b/test/test_helper.exs index 869559e..8db3eb9 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -1 +1,3 @@ ExUnit.start() + +AshPostgres.TestRepo.start_link()