diff --git a/.iex.exs b/.iex.exs index e69de29..264c294 100644 --- a/.iex.exs +++ b/.iex.exs @@ -0,0 +1,8 @@ +IEx.configure(inspect: [charlists: false]) + +alias ZeroPhoenix.Accounts +alias ZeroPhoenix.Accounts.{Friendship, Person} + +alias ZeroPhoenix.Repo + +import Ecto.Query diff --git a/README.md b/README.md index c0f36b9..d3d3adf 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ The purpose of this example is to provide details as to how one would go about u - Elixir 1.13.2 or newer -- Erlang 24.2.1 or newer +- Erlang 24.2 or newer - Phoenix 1.6.6 or newer @@ -126,7 +126,7 @@ Note: This tutorial was updated on macOS 11.6.3. 5. generate contexts, schemas, and migrations for the `Person` resource ```zsh - mix phx.gen.context Account Person people first_name:string last_name:string username:string email:string + mix phx.gen.context Accounts Person people first_name:string last_name:string username:string email:string ``` 6. replace the generated `Person` schema with the following: @@ -134,20 +134,22 @@ Note: This tutorial was updated on macOS 11.6.3. `lib/zero_phoenix/account/person.ex`: ```elixir - defmodule ZeroPhoenix.Account.Person do + defmodule ZeroPhoenix.Accounts.Person do use Ecto.Schema + import Ecto.Changeset - alias ZeroPhoenix.Account.Person - alias ZeroPhoenix.Account.Friendship + + alias ZeroPhoenix.Accounts.Friendship + alias ZeroPhoenix.Accounts.Person schema "people" do - field(:email, :string) - field(:first_name, :string) - field(:last_name, :string) - field(:username, :string) + field :email, :string + field :first_name, :string + field :last_name, :string + field :username, :string - has_many(:friendships, Friendship) - has_many(:friends, through: [:friendships, :friend]) + has_many :friendships, Friendship + has_many :friends, through: [:friendships, :friend] timestamps() end @@ -170,7 +172,7 @@ Note: This tutorial was updated on macOS 11.6.3. 8. generate contexts, schemas, and migrations for the `Friendship` resource ```zsh - mix phx.gen.context Account Friendship friendships person_id:references:people friend_id:references:people + mix phx.gen.context Accounts Friendship friendships person_id:references:people friend_id:references:people ``` 9. replace the generated `CreateFriendship` migration with the following: @@ -197,20 +199,22 @@ Note: This tutorial was updated on macOS 11.6.3. 10. replace the generated `Friendship` schema with the following: - `lib/zero_phoenix/account/friendship.ex`: + `lib/zero_phoenix/accounts/friendship.ex`: ```elixir - defmodule ZeroPhoenix.Account.Friendship do + defmodule ZeroPhoenix.Accounts.Friendship do use Ecto.Schema + import Ecto.Changeset - alias ZeroPhoenix.Account.Person - alias ZeroPhoenix.Account.Friendship + + alias ZeroPhoenix.Accounts.Friendship + alias ZeroPhoenix.Accounts.Person @required_fields [:person_id, :friend_id] schema "friendships" do - belongs_to(:person, Person) - belongs_to(:friend, Person) + belongs_to :person, Person + belongs_to :friend, Person timestamps() end @@ -261,7 +265,7 @@ Note: This tutorial was updated on macOS 11.6.3. ```zsh defmodule ZeroPhoenix.Seeds do - alias ZeroPhoenix.Account.{Person, Friendship} + alias ZeroPhoenix.Accounts.{Person, Friendship} alias ZeroPhoenix.Repo def run() do @@ -375,7 +379,9 @@ Note: This tutorial was updated on macOS 11.6.3. mix run priv/repo/seeds.exs ``` -17. add `absinthe_plug` package to your `mix.exs` dependencies as follows: +17. add `absinthe_plug` and `ataloader` hex package dependencies as follows: + + `mix.exs`: ```elixir defp deps do @@ -391,8 +397,10 @@ Note: This tutorial was updated on macOS 11.6.3. {:gettext, "~> 0.18.2"}, {:jason, "~> 1.2.2"}, {:plug_cowboy, "~> 2.5.2"}, + {:absinthe, "~> 1.7.0"}, {:absinthe_plug, "~> 1.5.8"}, - {:cors_plug, "~> 2.0.3"} + {:cors_plug, "~> 2.0.3"}, + {:dataloader, "~> 1.0.10"} ] end ``` @@ -417,42 +425,36 @@ Note: This tutorial was updated on macOS 11.6.3. plug(ZeroPhoenixWeb.Router) ``` -20. add the GraphQL schema which represents our entry point into our GraphQL structure: +20. create the GraphQL directory structure + + ```zsh + mkdir -p lib/zero_phoenix_web/graphql/{resolvers,schemas/{queries,mutations},types} + ``` + +21. add the GraphQL schema which represents our entry point into our GraphQL structure: `lib/zero_phoenix_web/graphql/schema.ex`: ```elixir - defmodule ZeroPhoenixWeb.Graphql.Schema do + defmodule ZeroPhoenixWeb.GraphQL.Schema do use Absinthe.Schema - import_types(ZeroPhoenixWeb.Graphql.Types.Person) + import_types(ZeroPhoenixWeb.GraphQL.Types.Person) - alias ZeroPhoenix.Account.Person - alias ZeroPhoenix.Account + import_types(ZeroPhoenixWeb.GraphQL.Schemas.Queries.Person) query do - field :person, type: :person do - arg(:id, non_null(:id)) - - resolve(fn %{id: id}, _info -> - case Account.get_person(id) do - %Person{} = person -> - {:ok, person} - - _error -> - {:error, "Person id #{id} not found"} - end - end) - end + import_fields(:person_queries) + end end ``` -21. add our Person type which will be performing queries against: +22. add our Person type which will be performing queries against: `lib/zero_phoenix_web/graphql/types/person.ex`: ```elixir - defmodule ZeroPhoenixWeb.Graphql.Types.Person do + defmodule ZeroPhoenixWeb.GraphQL.Types.Person do use Absinthe.Schema.Notation import Ecto @@ -486,7 +488,46 @@ Note: This tutorial was updated on macOS 11.6.3. end ``` -22. add routes for our GraphQL API and GraphiQL browser endpoints: +23. add the `person_queries` object to contain all the queries for a person: + + `lib/zero_phoenix_web/graphql/schemas/queries/person.ex`: + + ```elixir + defmodule ZeroPhoenixWeb.GraphQL.Schemas.Queries.Person do + use Absinthe.Schema.Notation + + object :person_queries do + field :person, type: :person do + arg :id, non_null(:id) + + resolve(&ZeroPhoenixWeb.GraphQL.Resolvers.PersonResolver.find/3) + end + end + end + ``` + +24. add the `PersonResolver` to fetch the individual fields of our person object: + + `lib/zero_phoenix_web/graphql/resolvers/person_resolver.ex`: + + ```elixir + defmodule ZeroPhoenixWeb.GraphQL.Resolvers.PersonResolver do + alias ZeroPhoenix.Accounts + alias ZeroPhoenix.Accounts.Person + + def find(_parent, %{id: id}, _info) do + case Accounts.get_person(id) do + %Person{} = person -> + {:ok, person} + + _error -> + {:error, "Person id #{id} not found"} + end + end + end + ``` + +25. add routes for our GraphQL API and GraphiQL browser endpoints: `lib/zero_phoenix_web/router.ex`: @@ -507,30 +548,30 @@ Note: This tutorial was updated on macOS 11.6.3. if Mix.env() in [:dev, :test] do forward "/graphiql", Absinthe.Plug.GraphiQL, - schema: ZeroPhoenixWeb.Graphql.Schema, + schema: ZeroPhoenixWeb.GraphQL.Schema, json_codec: Jason, interface: :playground end - forward "/graphql", + forward "/api", Absinthe.Plug, - schema: ZeroPhoenixWeb.Graphql.Schema + schema: ZeroPhoenixWeb.GraphQL.Schema end ``` -23. start the server +26. start the server ```zsh mix phx.server ``` -24. navigate to our application within the browser +27. navigate to our application within the browser ```zsh open http://localhost:4000/graphiql ``` -25. enter the below GraphQL query on the left side of the browser window +28. enter the below GraphQL query on the left side of the browser window ```graphql { @@ -549,7 +590,7 @@ Note: This tutorial was updated on macOS 11.6.3. } ``` -26. run the GraphQL query +29. run the GraphQL query ```text Control + Enter diff --git a/dev/support/seeds.ex b/dev/support/seeds.ex index eb3d36c..0a07fd4 100644 --- a/dev/support/seeds.ex +++ b/dev/support/seeds.ex @@ -1,5 +1,5 @@ defmodule ZeroPhoenix.Seeds do - alias ZeroPhoenix.Account.{Person, Friendship} + alias ZeroPhoenix.Accounts.{Person, Friendship} alias ZeroPhoenix.Repo def run() do diff --git a/lib/zero_phoenix/account/person.ex b/lib/zero_phoenix/account/person.ex deleted file mode 100644 index eb0da84..0000000 --- a/lib/zero_phoenix/account/person.ex +++ /dev/null @@ -1,25 +0,0 @@ -defmodule ZeroPhoenix.Account.Person do - use Ecto.Schema - import Ecto.Changeset - alias ZeroPhoenix.Account.Person - alias ZeroPhoenix.Account.Friendship - - schema "people" do - field(:email, :string) - field(:first_name, :string) - field(:last_name, :string) - field(:username, :string) - - has_many(:friendships, Friendship) - has_many(:friends, through: [:friendships, :friend]) - - timestamps() - end - - @doc false - def changeset(%Person{} = person, attrs) do - person - |> cast(attrs, [:first_name, :last_name, :username, :email]) - |> validate_required([:first_name, :last_name, :username, :email]) - end -end diff --git a/lib/zero_phoenix/account/account.ex b/lib/zero_phoenix/accounts.ex similarity index 95% rename from lib/zero_phoenix/account/account.ex rename to lib/zero_phoenix/accounts.ex index 66491c0..4935529 100644 --- a/lib/zero_phoenix/account/account.ex +++ b/lib/zero_phoenix/accounts.ex @@ -1,12 +1,12 @@ -defmodule ZeroPhoenix.Account do +defmodule ZeroPhoenix.Accounts do @moduledoc """ - The Account context. + The Accounts context. """ import Ecto.Query, warn: false - alias ZeroPhoenix.Repo - alias ZeroPhoenix.Account.Person + alias ZeroPhoenix.Repo + alias ZeroPhoenix.Accounts.Person @doc """ Returns the list of people. diff --git a/lib/zero_phoenix/account/friendship.ex b/lib/zero_phoenix/accounts/friendship.ex similarity index 62% rename from lib/zero_phoenix/account/friendship.ex rename to lib/zero_phoenix/accounts/friendship.ex index 3a923e2..a1e3842 100644 --- a/lib/zero_phoenix/account/friendship.ex +++ b/lib/zero_phoenix/accounts/friendship.ex @@ -1,14 +1,16 @@ -defmodule ZeroPhoenix.Account.Friendship do +defmodule ZeroPhoenix.Accounts.Friendship do use Ecto.Schema + import Ecto.Changeset - alias ZeroPhoenix.Account.Person - alias ZeroPhoenix.Account.Friendship + + alias ZeroPhoenix.Accounts.Friendship + alias ZeroPhoenix.Accounts.Person @required_fields [:person_id, :friend_id] schema "friendships" do - belongs_to(:person, Person) - belongs_to(:friend, Person) + belongs_to :person, Person + belongs_to :friend, Person timestamps() end diff --git a/lib/zero_phoenix/accounts/person.ex b/lib/zero_phoenix/accounts/person.ex new file mode 100644 index 0000000..58bf94e --- /dev/null +++ b/lib/zero_phoenix/accounts/person.ex @@ -0,0 +1,27 @@ +defmodule ZeroPhoenix.Accounts.Person do + use Ecto.Schema + + import Ecto.Changeset + + alias ZeroPhoenix.Accounts.Friendship + alias ZeroPhoenix.Accounts.Person + + schema "people" do + field :email, :string + field :first_name, :string + field :last_name, :string + field :username, :string + + has_many :friendships, Friendship + has_many :friends, through: [:friendships, :friend] + + timestamps() + end + + @doc false + def changeset(%Person{} = person, attrs) do + person + |> cast(attrs, [:first_name, :last_name, :username, :email]) + |> validate_required([:first_name, :last_name, :username, :email]) + end +end diff --git a/lib/zero_phoenix_web/graphql/resolvers/person_resolver.ex b/lib/zero_phoenix_web/graphql/resolvers/person_resolver.ex new file mode 100644 index 0000000..a1f3a93 --- /dev/null +++ b/lib/zero_phoenix_web/graphql/resolvers/person_resolver.ex @@ -0,0 +1,35 @@ +defmodule ZeroPhoenixWeb.GraphQL.Resolvers.PersonResolver do + import Ecto + + alias ZeroPhoenix.Accounts + alias ZeroPhoenix.Accounts.Person + alias ZeroPhoenix.Repo + + def find(_parent, %{id: id}, _resolution) do + case Accounts.get_person(id) do + %Person{} = person -> + {:ok, person} + + _error -> + {:error, "Person id #{id} not found"} + end + end + + def list(_parent, %{ids: ids}, _resolution) do + {:ok, Accounts.get_people(ids)} + end + + def create(_parent, %{input: params}, _resolution) do + case Accounts.create_person(params) do + {:ok, person} -> + {:ok, person} + + {:error, _} -> + {:error, "Could not create person"} + end + end + + def friends(parent, _args, _resolution) do + {:ok, Repo.all(assoc(parent, :friends))} + end +end diff --git a/lib/zero_phoenix_web/graphql/schema.ex b/lib/zero_phoenix_web/graphql/schema.ex index 7d9d318..2675d3e 100644 --- a/lib/zero_phoenix_web/graphql/schema.ex +++ b/lib/zero_phoenix_web/graphql/schema.ex @@ -1,32 +1,17 @@ -defmodule ZeroPhoenixWeb.Graphql.Schema do +defmodule ZeroPhoenixWeb.GraphQL.Schema do use Absinthe.Schema - import_types(ZeroPhoenixWeb.Graphql.Types.Person) - - alias ZeroPhoenix.Account.Person - alias ZeroPhoenix.Account + import_types(ZeroPhoenixWeb.GraphQL.Types.Person) + + import_types(ZeroPhoenixWeb.GraphQL.Schemas.Queries.Person) query do - field :person, type: :person do - arg(:id, non_null(:id)) - - resolve(fn %{id: id}, _info -> - case Account.get_person(id) do - %Person{} = person -> - {:ok, person} - - _error -> - {:error, "Person id #{id} not found"} - end - end) - end + import_fields(:person_queries) + end - field :people, type: list_of(:person) do - arg(:ids, list_of(:id), default_value: []) + import_types(ZeroPhoenixWeb.GraphQL.Schemas.Mutations.Person) - resolve(fn %{ids: ids}, _info -> - {:ok, Account.get_people(ids)} - end) - end + mutation do + import_fields(:person_mutations) end end diff --git a/lib/zero_phoenix_web/graphql/schemas/mutations/person.ex b/lib/zero_phoenix_web/graphql/schemas/mutations/person.ex new file mode 100644 index 0000000..3832f8c --- /dev/null +++ b/lib/zero_phoenix_web/graphql/schemas/mutations/person.ex @@ -0,0 +1,11 @@ +defmodule ZeroPhoenixWeb.GraphQL.Schemas.Mutations.Person do + use Absinthe.Schema.Notation + + object :person_mutations do + field :create_person, type: :person do + arg :input, non_null(:person_input) + + resolve(&ZeroPhoenixWeb.GraphQL.Resolvers.PersonResolver.create/3) + end + end +end diff --git a/lib/zero_phoenix_web/graphql/schemas/queries/person.ex b/lib/zero_phoenix_web/graphql/schemas/queries/person.ex new file mode 100644 index 0000000..039a002 --- /dev/null +++ b/lib/zero_phoenix_web/graphql/schemas/queries/person.ex @@ -0,0 +1,17 @@ +defmodule ZeroPhoenixWeb.GraphQL.Schemas.Queries.Person do + use Absinthe.Schema.Notation + + object :person_queries do + field :person, type: :person do + arg :id, non_null(:id) + + resolve(&ZeroPhoenixWeb.GraphQL.Resolvers.PersonResolver.find/3) + end + + field :people, type: list_of(:person) do + arg(:ids, list_of(:id), default_value: []) + + resolve(&ZeroPhoenixWeb.GraphQL.Resolvers.PersonResolver.list/3) + end + end +end diff --git a/lib/zero_phoenix_web/graphql/types/person.ex b/lib/zero_phoenix_web/graphql/types/person.ex index f30681c..8a919d1 100644 --- a/lib/zero_phoenix_web/graphql/types/person.ex +++ b/lib/zero_phoenix_web/graphql/types/person.ex @@ -1,9 +1,7 @@ defmodule ZeroPhoenixWeb.Graphql.Types.Person do use Absinthe.Schema.Notation - import Ecto - - alias ZeroPhoenix.Repo + alias ZeroPhoenixWeb.Graphql.Resolvers @desc "a person" object :person do @@ -24,9 +22,22 @@ defmodule ZeroPhoenixWeb.Graphql.Types.Person do @desc "a list of friends for our person" field :friends, list_of(:person) do - resolve(fn _, %{source: person} -> - {:ok, Repo.all(assoc(person, :friends))} - end) + resolve &Resolvers.PersonResolver.friends/3 end end + + @desc "a person input" + input_object :person_input do + @desc "first name of a person" + field(:first_name, non_null(:string)) + + @desc "last name of a person" + field(:last_name, non_null(:string)) + + @desc "username of a person" + field(:username, non_null(:string)) + + @desc "email of a person" + field(:email, non_null(:string)) + end end diff --git a/lib/zero_phoenix_web/router.ex b/lib/zero_phoenix_web/router.ex index 946125a..fc16b10 100644 --- a/lib/zero_phoenix_web/router.ex +++ b/lib/zero_phoenix_web/router.ex @@ -8,17 +8,17 @@ defmodule ZeroPhoenixWeb.Router do scope "/" do pipe_through :api - if Mix.env() == :dev do + if Mix.env() in [:dev, :test] do forward "/graphiql", Absinthe.Plug.GraphiQL, - schema: ZeroPhoenixWeb.Graphql.Schema, + schema: ZeroPhoenixWeb.GraphQL.Schema, json_codec: Jason, interface: :playground end - forward "/graphql", + forward "/api", Absinthe.Plug, - schema: ZeroPhoenixWeb.Graphql.Schema + schema: ZeroPhoenixWeb.GraphQL.Schema end # Enables LiveDashboard only for development diff --git a/mix.exs b/mix.exs index a47258f..ff29449 100644 --- a/mix.exs +++ b/mix.exs @@ -5,7 +5,7 @@ defmodule ZeroPhoenix.Mixfile do [ app: :zero_phoenix, version: "3.3.4", - elixir: "~> 1.13.2", + elixir: "~> 1.13.3", elixirc_paths: elixirc_paths(Mix.env()), compilers: [:gettext] ++ Mix.compilers(), start_permanent: Mix.env() == :prod, @@ -45,6 +45,7 @@ defmodule ZeroPhoenix.Mixfile do {:gettext, "~> 0.18.2"}, {:jason, "~> 1.2.2"}, {:plug_cowboy, "~> 2.5.2"}, + {:absinthe, "~> 1.7.0"}, {:absinthe_plug, "~> 1.5.8"}, {:cors_plug, "~> 2.0.3"} ] diff --git a/mix.lock b/mix.lock index a1261f5..bbaac1a 100644 --- a/mix.lock +++ b/mix.lock @@ -1,5 +1,6 @@ %{ "absinthe": {:hex, :absinthe, "1.7.0", "36819e7b1fd5046c9c734f27fe7e564aed3bda59f0354c37cd2df88fd32dd014", [:mix], [{:dataloader, "~> 1.0.0", [hex: :dataloader, repo: "hexpm", optional: true]}, {:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}, {:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0 or ~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "566a5b5519afc9b29c4d367f0c6768162de3ec03e9bf9916f9dc2bcbe7c09643"}, + "absinthe_phoenix": {:hex, :absinthe_phoenix, "2.0.2", "e607b438db900049b9b3760f8ecd0591017a46122fffed7057bf6989020992b5", [:mix], [{:absinthe, "~> 1.5", [hex: :absinthe, repo: "hexpm", optional: false]}, {:absinthe_plug, "~> 1.5", [hex: :absinthe_plug, repo: "hexpm", optional: false]}, {:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.5", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.13 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}], "hexpm", "d36918925c380dc7d2ed7d039c9a3b4182ec36723f7417a68745ade5aab22f8d"}, "absinthe_plug": {:hex, :absinthe_plug, "1.5.8", "38d230641ba9dca8f72f1fed2dfc8abd53b3907d1996363da32434ab6ee5d6ab", [:mix], [{:absinthe, "~> 1.5", [hex: :absinthe, repo: "hexpm", optional: false]}, {:plug, "~> 1.4", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "bbb04176647b735828861e7b2705465e53e2cf54ccf5a73ddd1ebd855f996e5a"}, "castore": {:hex, :castore, "0.1.13", "ccf3ab251ffaebc4319f41d788ce59a6ab3f42b6c27e598ad838ffecee0b04f9", [:mix], [], "hexpm", "a14a7eecfec7e20385493dbb92b0d12c5d77ecfd6307de10102d58c94e8c49c0"}, "connection": {:hex, :connection, "1.1.0", "ff2a49c4b75b6fb3e674bfc5536451607270aac754ffd1bdfe175abe4a6d7a68", [:mix], [], "hexpm", "722c1eb0a418fbe91ba7bd59a47e28008a189d47e37e0e7bb85585a016b2869c"}, @@ -7,6 +8,7 @@ "cowboy": {:hex, :cowboy, "2.9.0", "865dd8b6607e14cf03282e10e934023a1bd8be6f6bacf921a7e2a96d800cd452", [:make, :rebar3], [{:cowlib, "2.11.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "2c729f934b4e1aa149aff882f57c6372c15399a20d54f65c8d67bef583021bde"}, "cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"}, "cowlib": {:hex, :cowlib, "2.11.0", "0b9ff9c346629256c42ebe1eeb769a83c6cb771a6ee5960bd110ab0b9b872063", [:make, :rebar3], [], "hexpm", "2b3e9da0b21c4565751a6d4901c20d1b4cc25cbb7fd50d91d2ab6dd287bc86a9"}, + "dataloader": {:hex, :dataloader, "1.0.10", "a42f07641b1a0572e0b21a2a5ae1be11da486a6790f3d0d14512d96ff3e3bbe9", [:mix], [{:ecto, ">= 3.4.3 and < 4.0.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:telemetry, "~> 1.0 or ~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "54cd70cec09addf4b2ace14cc186a283a149fd4d3ec5475b155951bf33cd963f"}, "db_connection": {:hex, :db_connection, "2.4.1", "6411f6e23f1a8b68a82fa3a36366d4881f21f47fc79a9efb8c615e62050219da", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ea36d226ec5999781a9a8ad64e5d8c4454ecedc7a4d643e4832bf08efca01f00"}, "decimal": {:hex, :decimal, "2.0.0", "a78296e617b0f5dd4c6caf57c714431347912ffb1d0842e998e9792b5642d697", [:mix], [], "hexpm", "34666e9c55dea81013e77d9d87370fe6cb6291d1ef32f46a1600230b1d44f577"}, "ecto": {:hex, :ecto, "3.7.1", "a20598862351b29f80f285b21ec5297da1181c0442687f9b8329f0445d228892", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "d36e5b39fc479e654cffd4dbe1865d9716e4a9b6311faff799b6f90ab81b8638"}, @@ -26,8 +28,8 @@ "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.3.3", "3a53772a6118d5679bf50fc1670505a290e32a1d195df9e069d8c53ab040c054", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "766796676e5f558dbae5d1bdb066849673e956005e3730dfd5affd7a6da4abac"}, "phoenix_live_view": {:hex, :phoenix_live_view, "0.17.5", "63f52a6f9f6983f04e424586ff897c016ecc5e4f8d1e2c22c2887af1c57215d8", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.5.9 or ~> 1.6.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.1", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c5586e6a3d4df71b8214c769d4f5eb8ece2b4001711a7ca0f97323c36958b0e3"}, "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.0.0", "a1ae76717bb168cdeb10ec9d92d1480fec99e3080f011402c0a2d68d47395ffb", [:mix], [], "hexpm", "c52d948c4f261577b9c6fa804be91884b381a7f8f18450c5045975435350f771"}, - "phoenix_view": {:hex, :phoenix_view, "1.1.0", "149f053830ec3c19a2a8a67c208885a26e4c2b92cc4a9d54e03b633d68ef9add", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "dd219f768b3d97a224ed11e8a83f4fd0f3bd490434d3950d7c51a2e597a762f1"}, - "plug": {:hex, :plug, "1.12.1", "645678c800601d8d9f27ad1aebba1fdb9ce5b2623ddb961a074da0b96c35187d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "d57e799a777bc20494b784966dc5fbda91eb4a09f571f76545b72a634ce0d30b"}, + "phoenix_view": {:hex, :phoenix_view, "1.1.2", "1b82764a065fb41051637872c7bd07ed2fdb6f5c3bd89684d4dca6e10115c95a", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "7ae90ad27b09091266f6adbb61e1d2516a7c3d7062c6789d46a7554ec40f3a56"}, + "plug": {:hex, :plug, "1.13.2", "33aba8e2b43ddd68d9d49b818ed2fb46da85f4ec3229bc4bcd0c981a640a4e71", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a95cdfe599e3524b98684376c3f3494cbfbc1f41fcddefc380cac3138dd7619d"}, "plug_cowboy": {:hex, :plug_cowboy, "2.5.2", "62894ccd601cf9597e2c23911ff12798a8a18d237e9739f58a6b04e4988899fe", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "ea6e87f774c8608d60c8d34022a7d073bd7680a0a013f049fc62bf35efea1044"}, "plug_crypto": {:hex, :plug_crypto, "1.2.2", "05654514ac717ff3a1843204b424477d9e60c143406aa94daf2274fdd280794d", [:mix], [], "hexpm", "87631c7ad914a5a445f0a3809f99b079113ae4ed4b867348dd9eec288cecb6db"}, "postgrex": {:hex, :postgrex, "0.15.13", "7794e697481799aee8982688c261901de493eb64451feee6ea58207d7266d54a", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "3ffb76e1a97cfefe5c6a95632a27ffb67f28871c9741fb585f9d1c3cd2af70f1"}, diff --git a/priv/repo/dataloader_example.exs b/priv/repo/dataloader_example.exs new file mode 100644 index 0000000..0d471ed --- /dev/null +++ b/priv/repo/dataloader_example.exs @@ -0,0 +1,39 @@ +alias ZeroPhoenix.Accounts + +# Fetch 3 places we want to get the bookings for: + +[person1, person2, person3] = Accounts.list_people([limit: 3]) + +# Create a dataloader: + +loader = Dataloader.new + +# Create a source which is the database repo: + +source = Dataloader.Ecto.new(ZeroPhoenix.Repo) + +# Add the source to the data loader. The name of the source is +# arbitrary, but typically named after a Phoenix context module. +# In this case, the name is `ZeroPhoenix.Accounts`. + +loader = loader |> Dataloader.add_source(Accounts, source) + +# Create a batch of bookings to be loaded (does not query the database): + +loader = + loader + |> Dataloader.load(Accounts, :friends, person1) + |> Dataloader.load(Accounts, :friends, person2) + |> Dataloader.load(Accounts, :friends, person3) + +# Now query the database to retrieve all queued-up friends as a batch: + +loader = loader |> Dataloader.run + +# 🔥 This runs one Ecto query to fetch all the bookings for all the places! + +# Now you can get the bookings for a particular place: + +loader |> Dataloader.load(Accounts, :friends, person1) |> IO.inspect +loader |> Dataloader.load(Accounts, :friends, person2) |> IO.inspect +loader |> Dataloader.load(Accounts, :friends, person3) |> IO.inspect diff --git a/test/support/data_case.ex b/test/support/data_case.ex index 068d705..2782f1d 100644 --- a/test/support/data_case.ex +++ b/test/support/data_case.ex @@ -38,7 +38,7 @@ defmodule ZeroPhoenix.DataCase do @doc """ A helper that transform changeset errors to a map of messages. - assert {:error, changeset} = Account.create_user(%{password: "short"}) + assert {:error, changeset} = Accounts.create_user(%{password: "short"}) assert "password is too short" in errors_on(changeset).password assert %{password: ["password is too short"]} = errors_on(changeset) diff --git a/test/zero_phoenix/accounts/accounts_test.exs b/test/zero_phoenix/accounts/accounts_test.exs index 7f775ff..4d8a8f3 100644 --- a/test/zero_phoenix/accounts/accounts_test.exs +++ b/test/zero_phoenix/accounts/accounts_test.exs @@ -1,10 +1,10 @@ -defmodule ZeroPhoenix.AccountTest do +defmodule ZeroPhoenix.AccountsTest do use ZeroPhoenix.DataCase - alias ZeroPhoenix.Account + alias ZeroPhoenix.Accounts describe "people" do - alias ZeroPhoenix.Account.Person + alias ZeroPhoenix.Accounts.Person @valid_attrs %{ email: "some email", @@ -24,23 +24,23 @@ defmodule ZeroPhoenix.AccountTest do {:ok, person} = attrs |> Enum.into(@valid_attrs) - |> Account.create_person() + |> Accounts.create_person() person end test "list_people/0 returns all people" do person = person_fixture() - assert Account.list_people() == [person] + assert Accounts.list_people() == [person] end test "get_person!/1 returns the person with given id" do person = person_fixture() - assert Account.get_person!(person.id) == person + assert Accounts.get_person!(person.id) == person end test "create_person/1 with valid data creates a person" do - assert {:ok, %Person{} = person} = Account.create_person(@valid_attrs) + assert {:ok, %Person{} = person} = Accounts.create_person(@valid_attrs) assert person.email == "some email" assert person.first_name == "some first_name" assert person.last_name == "some last_name" @@ -48,12 +48,12 @@ defmodule ZeroPhoenix.AccountTest do end test "create_person/1 with invalid data returns error changeset" do - assert {:error, %Ecto.Changeset{}} = Account.create_person(@invalid_attrs) + assert {:error, %Ecto.Changeset{}} = Accounts.create_person(@invalid_attrs) end test "update_person/2 with valid data updates the person" do person = person_fixture() - assert {:ok, person} = Account.update_person(person, @update_attrs) + assert {:ok, person} = Accounts.update_person(person, @update_attrs) assert %Person{} = person assert person.email == "some updated email" assert person.first_name == "some updated first_name" @@ -63,19 +63,19 @@ defmodule ZeroPhoenix.AccountTest do test "update_person/2 with invalid data returns error changeset" do person = person_fixture() - assert {:error, %Ecto.Changeset{}} = Account.update_person(person, @invalid_attrs) - assert person == Account.get_person!(person.id) + assert {:error, %Ecto.Changeset{}} = Accounts.update_person(person, @invalid_attrs) + assert person == Accounts.get_person!(person.id) end test "delete_person/1 deletes the person" do person = person_fixture() - assert {:ok, %Person{}} = Account.delete_person(person) - assert_raise Ecto.NoResultsError, fn -> Account.get_person!(person.id) end + assert {:ok, %Person{}} = Accounts.delete_person(person) + assert_raise Ecto.NoResultsError, fn -> Accounts.get_person!(person.id) end end test "change_person/1 returns a person changeset" do person = person_fixture() - assert %Ecto.Changeset{} = Account.change_person(person) + assert %Ecto.Changeset{} = Accounts.change_person(person) end end end diff --git a/test/zero_phoenix_web/graphql/schemas/mutations/person_test.exs b/test/zero_phoenix_web/graphql/schemas/mutations/person_test.exs new file mode 100644 index 0000000..5d59a46 --- /dev/null +++ b/test/zero_phoenix_web/graphql/schemas/mutations/person_test.exs @@ -0,0 +1,42 @@ +defmodule ZeroPhoenixWeb.GraphQL.Schemas.Mutations.PersonTest do + use ZeroPhoenixWeb.ConnCase, async: true + + test "create person" do + query = """ + mutation CreatePerson($person: PersonInput!) { + createPerson(input: $person) { + firstName + lastName + email + username + } + } + """ + + person = %{ + "firstName" => "Jane", + "lastName" => "Doe", + "email" => "jane@example.com", + "username" => "janed" + } + + response = + post( + build_conn(), + "/api", + query: query, + variables: %{"person" => person} + ) + + assert json_response(response, 200) == %{ + "data" => %{ + "createPerson" => %{ + "firstName" => person["firstName"], + "lastName" => person["lastName"], + "email" => person["email"], + "username" => person["username"] + } + } + } + end +end diff --git a/test/zero_phoenix_web/schema/query/people_test.exs b/test/zero_phoenix_web/graphql/schemas/queries/person_test.exs similarity index 71% rename from test/zero_phoenix_web/schema/query/people_test.exs rename to test/zero_phoenix_web/graphql/schemas/queries/person_test.exs index 1dc3002..4cb75b2 100644 --- a/test/zero_phoenix_web/schema/query/people_test.exs +++ b/test/zero_phoenix_web/graphql/schemas/queries/person_test.exs @@ -1,15 +1,46 @@ -defmodule ZeroPhoenixWeb.Schema.Query.PeopleTest do +defmodule ZeroPhoenixWeb.GraphQL.Schemas.Queries.PersonTest do use ZeroPhoenixWeb.ConnCase, async: true import Ecto.Query - alias ZeroPhoenix.Account.Person + alias ZeroPhoenix.Accounts.Person alias ZeroPhoenix.Repo setup do ZeroPhoenix.Seeds.run() end + test "get person by ID" do + query = """ + query GetPerson($personId: ID!) { + person(id: $personId) { + email + } + } + """ + + person = + Person + |> first() + |> Repo.one() + + response = + post( + build_conn(), + "/api", + query: query, + variables: %{"personId" => person.id} + ) + + assert json_response(response, 200) == %{ + "data" => %{ + "person" => %{ + "email" => "conradwt@gmail.com" + } + } + } + end + test "get people by IDs" do query = """ query GetPeople($ids: [ID]) { @@ -31,7 +62,7 @@ defmodule ZeroPhoenixWeb.Schema.Query.PeopleTest do response = post( build_conn(), - "/graphql", + "/api", query: query, variables: %{"ids" => people_ids} ) @@ -65,7 +96,7 @@ defmodule ZeroPhoenixWeb.Schema.Query.PeopleTest do response = post( build_conn(), - "/graphql", + "/api", query: query ) diff --git a/test/zero_phoenix_web/schema/query/person_test.exs b/test/zero_phoenix_web/schema/query/person_test.exs deleted file mode 100644 index 1ddc698..0000000 --- a/test/zero_phoenix_web/schema/query/person_test.exs +++ /dev/null @@ -1,43 +0,0 @@ -defmodule ZeroPhoenixWeb.Schema.Query.PersonTest do - use ZeroPhoenixWeb.ConnCase, async: true - - import Ecto.Query, only: [first: 1] - - alias ZeroPhoenix.Account.Person - alias ZeroPhoenix.Repo - - setup do - ZeroPhoenix.Seeds.run() - end - - test "get person by ID" do - query = """ - query GetPerson($personId: ID!) { - person(id: $personId) { - email - } - } - """ - - person = - Person - |> first() - |> Repo.one() - - response = - post( - build_conn(), - "/graphql", - query: query, - variables: %{"personId" => person.id} - ) - - assert json_response(response, 200) == %{ - "data" => %{ - "person" => %{ - "email" => "conradwt@gmail.com" - } - } - } - end -end