├── test ├── test_helper.exs ├── web │ ├── views │ │ └── error_view_test.exs │ └── controllers │ │ ├── post_controller_test.exs │ │ └── user_controller_test.exs ├── support │ ├── channel_case.ex │ ├── conn_case.ex │ └── data_case.ex ├── blog_test.exs └── accounts_test.exs ├── lib └── blog_app │ ├── blog │ ├── post_resolver.ex │ ├── post.ex │ └── blog.ex │ ├── accounts │ ├── user_resolver.ex │ ├── user.ex │ └── accounts.ex │ ├── repo.ex │ ├── web │ ├── schema.ex │ ├── views │ │ ├── error_view.ex │ │ ├── post_view.ex │ │ ├── user_view.ex │ │ ├── changeset_view.ex │ │ └── error_helpers.ex │ ├── schema │ │ └── types.ex │ ├── router.ex │ ├── controllers │ │ ├── fallback_controller.ex │ │ ├── post_controller.ex │ │ └── user_controller.ex │ ├── gettext.ex │ ├── channels │ │ └── user_socket.ex │ ├── web.ex │ └── endpoint.ex │ └── application.ex ├── priv ├── repo │ ├── migrations │ │ ├── 20170417205332_modify_blog_post.exs │ │ ├── 20170417192256_create_accounts_user.exs │ │ └── 20170417192302_create_blog_post.exs │ └── seeds.exs └── gettext │ ├── en │ └── LC_MESSAGES │ │ └── errors.po │ └── errors.pot ├── .gitignore ├── config ├── test.exs ├── config.exs ├── dev.exs └── prod.exs ├── README.md ├── mix.exs └── mix.lock /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | 3 | Ecto.Adapters.SQL.Sandbox.mode(BlogApp.Repo, :manual) 4 | 5 | -------------------------------------------------------------------------------- /lib/blog_app/blog/post_resolver.ex: -------------------------------------------------------------------------------- 1 | defmodule BlogApp.Blog.PostResolver do 2 | alias BlogApp.{Blog.Post, Repo} 3 | 4 | def all(_args, _info) do 5 | {:ok, Repo.all(Post)} 6 | end 7 | end -------------------------------------------------------------------------------- /lib/blog_app/accounts/user_resolver.ex: -------------------------------------------------------------------------------- 1 | defmodule BlogApp.Accounts.UserResolver do 2 | alias BlogApp.{Accounts.User, Repo} 3 | 4 | def all(_args, _info) do 5 | {:ok, Repo.all(User)} 6 | end 7 | end -------------------------------------------------------------------------------- /lib/blog_app/blog/post.ex: -------------------------------------------------------------------------------- 1 | defmodule BlogApp.Blog.Post do 2 | use Ecto.Schema 3 | 4 | schema "blog_posts" do 5 | field :body, :string 6 | field :title, :string 7 | belongs_to :accounts_users, BlogApp.Accounts.User, foreign_key: :accounts_users_id 8 | 9 | timestamps() 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/blog_app/accounts/user.ex: -------------------------------------------------------------------------------- 1 | defmodule BlogApp.Accounts.User do 2 | use Ecto.Schema 3 | 4 | schema "accounts_users" do 5 | field :email, :string 6 | field :name, :string 7 | has_many :blog_posts, BlogApp.Blog.Post, foreign_key: :accounts_users_id 8 | 9 | timestamps() 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/blog_app/repo.ex: -------------------------------------------------------------------------------- 1 | defmodule BlogApp.Repo do 2 | use Ecto.Repo, otp_app: :blog_app 3 | 4 | @doc """ 5 | Dynamically loads the repository url from the 6 | DATABASE_URL environment variable. 7 | """ 8 | def init(_, opts) do 9 | {:ok, Keyword.put(opts, :url, System.get_env("DATABASE_URL"))} 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /priv/repo/migrations/20170417205332_modify_blog_post.exs: -------------------------------------------------------------------------------- 1 | defmodule BlogApp.Repo.Migrations.CreateBlogApp.Blog.Post do 2 | use Ecto.Migration 3 | 4 | def change do 5 | alter table(:blog_posts) do 6 | modify :body, :text 7 | end 8 | 9 | create index(:blog_posts, [:accounts_users_id]) 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /priv/repo/migrations/20170417192256_create_accounts_user.exs: -------------------------------------------------------------------------------- 1 | defmodule BlogApp.Repo.Migrations.CreateBlogApp.Accounts.User do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:accounts_users) do 6 | add :name, :string 7 | add :email, :string 8 | 9 | timestamps() 10 | end 11 | 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/blog_app/web/schema.ex: -------------------------------------------------------------------------------- 1 | defmodule BlogApp.Schema do 2 | use Absinthe.Schema 3 | import_types BlogApp.Schema.Types 4 | 5 | query do 6 | field :blog_posts, list_of(:blog_post) do 7 | resolve &BlogApp.Blog.PostResolver.all/2 8 | end 9 | 10 | field :accounts_users, list_of(:accounts_user) do 11 | resolve &BlogApp.Accounts.UserResolver.all/2 12 | end 13 | end 14 | end -------------------------------------------------------------------------------- /priv/repo/migrations/20170417192302_create_blog_post.exs: -------------------------------------------------------------------------------- 1 | defmodule BlogApp.Repo.Migrations.CreateBlogApp.Blog.Post do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:blog_posts) do 6 | add :title, :string 7 | add :body, :string 8 | add :accounts_users_id, references(:accounts_users, on_delete: :nothing) 9 | 10 | timestamps() 11 | end 12 | 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # App artifacts 2 | /_build 3 | /db 4 | /deps 5 | /*.ez 6 | 7 | # Generated on crash by the VM 8 | erl_crash.dump 9 | 10 | # Files matching config/*.secret.exs pattern contain sensitive 11 | # data and you should not commit them into version control. 12 | # 13 | # Alternatively, you may comment the line below and commit the 14 | # secrets files as long as you replace their contents by environment 15 | # variables. 16 | /config/*.secret.exs 17 | -------------------------------------------------------------------------------- /lib/blog_app/web/views/error_view.ex: -------------------------------------------------------------------------------- 1 | defmodule BlogApp.Web.ErrorView do 2 | use BlogApp.Web, :view 3 | 4 | def render("404.json", _assigns) do 5 | %{errors: %{detail: "Page not found"}} 6 | end 7 | 8 | def render("500.json", _assigns) do 9 | %{errors: %{detail: "Internal server error"}} 10 | end 11 | 12 | # In case no render clause matches or no 13 | # template is found, let's render it as 500 14 | def template_not_found(_template, assigns) do 15 | render "500.json", assigns 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/blog_app/web/views/post_view.ex: -------------------------------------------------------------------------------- 1 | defmodule BlogApp.Web.PostView do 2 | use BlogApp.Web, :view 3 | alias BlogApp.Web.PostView 4 | 5 | def render("index.json", %{posts: posts}) do 6 | %{data: render_many(posts, PostView, "post.json")} 7 | end 8 | 9 | def render("show.json", %{post: post}) do 10 | %{data: render_one(post, PostView, "post.json")} 11 | end 12 | 13 | def render("post.json", %{post: post}) do 14 | %{id: post.id, 15 | title: post.title, 16 | body: post.body} 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/blog_app/web/views/user_view.ex: -------------------------------------------------------------------------------- 1 | defmodule BlogApp.Web.UserView do 2 | use BlogApp.Web, :view 3 | alias BlogApp.Web.UserView 4 | 5 | def render("index.json", %{users: users}) do 6 | %{data: render_many(users, UserView, "user.json")} 7 | end 8 | 9 | def render("show.json", %{user: user}) do 10 | %{data: render_one(user, UserView, "user.json")} 11 | end 12 | 13 | def render("user.json", %{user: user}) do 14 | %{id: user.id, 15 | name: user.name, 16 | email: user.email} 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/blog_app/web/schema/types.ex: -------------------------------------------------------------------------------- 1 | defmodule BlogApp.Schema.Types do 2 | use Absinthe.Schema.Notation 3 | use Absinthe.Ecto, repo: BlogApp.Repo 4 | 5 | object :accounts_user do 6 | field :id, :id 7 | field :name, :string 8 | field :email, :string 9 | field :posts, list_of(:blog_post), resolve: assoc(:blog_posts) 10 | end 11 | 12 | object :blog_post do 13 | field :id, :id 14 | field :title, :string 15 | field :body, :string 16 | field :user, :accounts_user, resolve: assoc(:accounts_users) 17 | end 18 | end -------------------------------------------------------------------------------- /config/test.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | # We don't run a server during test. If one is required, 4 | # you can enable the server option below. 5 | config :blog_app, BlogApp.Web.Endpoint, 6 | http: [port: 4001], 7 | server: false 8 | 9 | # Print only warnings and errors during test 10 | config :logger, level: :warn 11 | 12 | # Configure your database 13 | config :blog_app, BlogApp.Repo, 14 | adapter: Ecto.Adapters.Postgres, 15 | username: "postgres", 16 | password: "postgres", 17 | database: "blog_app_test", 18 | hostname: "localhost", 19 | pool: Ecto.Adapters.SQL.Sandbox 20 | -------------------------------------------------------------------------------- /lib/blog_app/web/router.ex: -------------------------------------------------------------------------------- 1 | defmodule BlogApp.Web.Router do 2 | use BlogApp.Web, :router 3 | 4 | pipeline :api do 5 | plug :accepts, ["json"] 6 | end 7 | 8 | scope "/api", BlogApp.Web do 9 | pipe_through :api 10 | 11 | # Users 12 | resources "/users", UserController, except: [:new, :edit] 13 | 14 | # Posts 15 | resources "/posts", PostController, except: [:new, :edit] 16 | end 17 | 18 | forward "/graph", Absinthe.Plug, 19 | schema: BlogApp.Schema 20 | 21 | forward "/graphiql", Absinthe.Plug.GraphiQL, 22 | schema: BlogApp.Schema 23 | end 24 | -------------------------------------------------------------------------------- /lib/blog_app/web/views/changeset_view.ex: -------------------------------------------------------------------------------- 1 | defmodule BlogApp.Web.ChangesetView do 2 | use BlogApp.Web, :view 3 | 4 | @doc """ 5 | Traverses and translates changeset errors. 6 | 7 | See `Ecto.Changeset.traverse_errors/2` and 8 | `BlogApp.Web.ErrorHelpers.translate_error/1` for more details. 9 | """ 10 | def translate_errors(changeset) do 11 | Ecto.Changeset.traverse_errors(changeset, &translate_error/1) 12 | end 13 | 14 | def render("error.json", %{changeset: changeset}) do 15 | # When encoded, the changeset returns its errors 16 | # as a JSON object. So we just pass it forward. 17 | %{errors: translate_errors(changeset)} 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/blog_app/web/controllers/fallback_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule BlogApp.Web.FallbackController do 2 | @moduledoc """ 3 | Translates controller action results into valid `Plug.Conn` responses. 4 | 5 | See `Phoenix.Controller.action_fallback/1` for more details. 6 | """ 7 | use BlogApp.Web, :controller 8 | 9 | def call(conn, {:error, %Ecto.Changeset{} = changeset}) do 10 | conn 11 | |> put_status(:unprocessable_entity) 12 | |> render(BlogApp.Web.ChangesetView, "error.json", changeset: changeset) 13 | end 14 | 15 | def call(conn, {:error, :not_found}) do 16 | conn 17 | |> put_status(:not_found) 18 | |> render(BlogApp.Web.ErrorView, :"404") 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /test/web/views/error_view_test.exs: -------------------------------------------------------------------------------- 1 | defmodule BlogApp.Web.ErrorViewTest do 2 | use BlogApp.Web.ConnCase, async: true 3 | 4 | # Bring render/3 and render_to_string/3 for testing custom views 5 | import Phoenix.View 6 | 7 | test "renders 404.json" do 8 | assert render(BlogApp.Web.ErrorView, "404.json", []) == 9 | %{errors: %{detail: "Page not found"}} 10 | end 11 | 12 | test "render 500.json" do 13 | assert render(BlogApp.Web.ErrorView, "500.json", []) == 14 | %{errors: %{detail: "Internal server error"}} 15 | end 16 | 17 | test "render any other" do 18 | assert render(BlogApp.Web.ErrorView, "505.json", []) == 19 | %{errors: %{detail: "Internal server error"}} 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BlogApp 2 | 3 | To start your Phoenix server: 4 | 5 | * Install dependencies with `mix deps.get` 6 | * Create and migrate your database with `mix ecto.create && mix ecto.migrate` 7 | * Start Phoenix endpoint with `mix phx.server` 8 | 9 | Now you can visit [`localhost:4000`](http://localhost:4000) from your browser. 10 | 11 | Ready to run in production? Please [check our deployment guides](http://www.phoenixframework.org/docs/deployment). 12 | 13 | ## Learn more 14 | 15 | * Official website: http://www.phoenixframework.org/ 16 | * Guides: http://phoenixframework.org/docs/overview 17 | * Docs: https://hexdocs.pm/phoenix 18 | * Mailing list: http://groups.google.com/group/phoenix-talk 19 | * Source: https://github.com/phoenixframework/phoenix 20 | -------------------------------------------------------------------------------- /lib/blog_app/web/gettext.ex: -------------------------------------------------------------------------------- 1 | defmodule BlogApp.Web.Gettext do 2 | @moduledoc """ 3 | A module providing Internationalization with a gettext-based API. 4 | 5 | By using [Gettext](https://hexdocs.pm/gettext), 6 | your module gains a set of macros for translations, for example: 7 | 8 | import BlogApp.Web.Gettext 9 | 10 | # Simple translation 11 | gettext "Here is the string to translate" 12 | 13 | # Plural translation 14 | ngettext "Here is the string to translate", 15 | "Here are the strings to translate", 16 | 3 17 | 18 | # Domain-based translation 19 | dgettext "errors", "Here is the error message to translate" 20 | 21 | See the [Gettext Docs](https://hexdocs.pm/gettext) for detailed usage. 22 | """ 23 | use Gettext, otp_app: :blog_app 24 | end 25 | -------------------------------------------------------------------------------- /lib/blog_app/application.ex: -------------------------------------------------------------------------------- 1 | defmodule BlogApp.Application do 2 | use Application 3 | 4 | # See http://elixir-lang.org/docs/stable/elixir/Application.html 5 | # for more information on OTP Applications 6 | def start(_type, _args) do 7 | import Supervisor.Spec 8 | 9 | # Define workers and child supervisors to be supervised 10 | children = [ 11 | # Start the Ecto repository 12 | supervisor(BlogApp.Repo, []), 13 | # Start the endpoint when the application starts 14 | supervisor(BlogApp.Web.Endpoint, []), 15 | # Start your own worker by calling: BlogApp.Worker.start_link(arg1, arg2, arg3) 16 | # worker(BlogApp.Worker, [arg1, arg2, arg3]), 17 | ] 18 | 19 | # See http://elixir-lang.org/docs/stable/elixir/Supervisor.html 20 | # for other strategies and supported options 21 | opts = [strategy: :one_for_one, name: BlogApp.Supervisor] 22 | Supervisor.start_link(children, opts) 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /priv/repo/seeds.exs: -------------------------------------------------------------------------------- 1 | # Script for populating the database. You can run it as: 2 | # 3 | # mix run priv/repo/seeds.exs 4 | # 5 | # Inside the script, you can read and write to any of your 6 | # repositories directly: 7 | # 8 | # BlogApp.Repo.insert!(%BlogApp.SomeSchema{}) 9 | # 10 | # We recommend using the bang functions (`insert!`, `update!` 11 | # and so on) as they will fail if something goes wrong. 12 | 13 | alias BlogApp.Repo 14 | alias BlogApp.Accounts.User 15 | alias BlogApp.Blog.Post 16 | 17 | # Create 10 seed users 18 | 19 | for _ <- 1..10 do 20 | Repo.insert!(%User{ 21 | name: Faker.Name.name, 22 | email: Faker.Internet.safe_email 23 | }) 24 | end 25 | 26 | # Create 40 seed posts 27 | 28 | for _ <- 1..40 do 29 | Repo.insert!(%Post{ 30 | title: Faker.Lorem.sentence, 31 | body: Faker.Lorem.paragraphs(%Range{first: 1, last: 5}) |> Enum.join("\n\n"), 32 | accounts_users_id: Enum.random(1..10) # Pick random user for post to belong to 33 | }) 34 | end 35 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Mix.Config module. 3 | # 4 | # This configuration file is loaded before any dependency and 5 | # is restricted to this project. 6 | use Mix.Config 7 | 8 | # General application configuration 9 | config :blog_app, 10 | ecto_repos: [BlogApp.Repo] 11 | 12 | # Configures the endpoint 13 | config :blog_app, BlogApp.Web.Endpoint, 14 | url: [host: "localhost"], 15 | secret_key_base: "QPwBGut7U8dzhA6DColYmI6XRV2rDWkqVM4VQMi7GYyZBYGH2ZxjmiX5aYHfanZ1", 16 | render_errors: [view: BlogApp.Web.ErrorView, accepts: ~w(json)], 17 | pubsub: [name: BlogApp.PubSub, 18 | adapter: Phoenix.PubSub.PG2] 19 | 20 | # Configures Elixir's Logger 21 | config :logger, :console, 22 | format: "$time $metadata[$level] $message\n", 23 | metadata: [:request_id] 24 | 25 | # Import environment specific config. This must remain at the bottom 26 | # of this file so it overrides the configuration defined above. 27 | import_config "#{Mix.env}.exs" 28 | -------------------------------------------------------------------------------- /test/support/channel_case.ex: -------------------------------------------------------------------------------- 1 | defmodule BlogApp.Web.ChannelCase do 2 | @moduledoc """ 3 | This module defines the test case to be used by 4 | channel tests. 5 | 6 | Such tests rely on `Phoenix.ChannelTest` and also 7 | import other functionality to make it easier 8 | to build common datastructures and query the data layer. 9 | 10 | Finally, if the test case interacts with the database, 11 | it cannot be async. For this reason, every test runs 12 | inside a transaction which is reset at the beginning 13 | of the test unless the test case is marked as async. 14 | """ 15 | 16 | use ExUnit.CaseTemplate 17 | 18 | using do 19 | quote do 20 | # Import conveniences for testing with channels 21 | use Phoenix.ChannelTest 22 | 23 | # The default endpoint for testing 24 | @endpoint BlogApp.Web.Endpoint 25 | end 26 | end 27 | 28 | 29 | setup tags do 30 | :ok = Ecto.Adapters.SQL.Sandbox.checkout(BlogApp.Repo) 31 | unless tags[:async] do 32 | Ecto.Adapters.SQL.Sandbox.mode(BlogApp.Repo, {:shared, self()}) 33 | end 34 | :ok 35 | end 36 | 37 | end 38 | -------------------------------------------------------------------------------- /lib/blog_app/web/views/error_helpers.ex: -------------------------------------------------------------------------------- 1 | defmodule BlogApp.Web.ErrorHelpers do 2 | @moduledoc """ 3 | Conveniences for translating and building error messages. 4 | """ 5 | 6 | @doc """ 7 | Translates an error message using gettext. 8 | """ 9 | def translate_error({msg, opts}) do 10 | # Because error messages were defined within Ecto, we must 11 | # call the Gettext module passing our Gettext backend. We 12 | # also use the "errors" domain as translations are placed 13 | # in the errors.po file. 14 | # Ecto will pass the :count keyword if the error message is 15 | # meant to be pluralized. 16 | # On your own code and templates, depending on whether you 17 | # need the message to be pluralized or not, this could be 18 | # written simply as: 19 | # 20 | # dngettext "errors", "1 file", "%{count} files", count 21 | # dgettext "errors", "is invalid" 22 | # 23 | if count = opts[:count] do 24 | Gettext.dngettext(BlogApp.Web.Gettext, "errors", msg, msg, count, opts) 25 | else 26 | Gettext.dgettext(BlogApp.Web.Gettext, "errors", msg, opts) 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /test/support/conn_case.ex: -------------------------------------------------------------------------------- 1 | defmodule BlogApp.Web.ConnCase do 2 | @moduledoc """ 3 | This module defines the test case to be used by 4 | tests that require setting up a connection. 5 | 6 | Such tests rely on `Phoenix.ConnTest` and also 7 | import other functionality to make it easier 8 | to build common datastructures and query the data layer. 9 | 10 | Finally, if the test case interacts with the database, 11 | it cannot be async. For this reason, every test runs 12 | inside a transaction which is reset at the beginning 13 | of the test unless the test case is marked as async. 14 | """ 15 | 16 | use ExUnit.CaseTemplate 17 | 18 | using do 19 | quote do 20 | # Import conveniences for testing with connections 21 | use Phoenix.ConnTest 22 | import BlogApp.Web.Router.Helpers 23 | 24 | # The default endpoint for testing 25 | @endpoint BlogApp.Web.Endpoint 26 | end 27 | end 28 | 29 | 30 | setup tags do 31 | :ok = Ecto.Adapters.SQL.Sandbox.checkout(BlogApp.Repo) 32 | unless tags[:async] do 33 | Ecto.Adapters.SQL.Sandbox.mode(BlogApp.Repo, {:shared, self()}) 34 | end 35 | {:ok, conn: Phoenix.ConnTest.build_conn()} 36 | end 37 | 38 | end 39 | -------------------------------------------------------------------------------- /lib/blog_app/web/controllers/post_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule BlogApp.Web.PostController do 2 | use BlogApp.Web, :controller 3 | 4 | alias BlogApp.Blog 5 | alias BlogApp.Blog.Post 6 | 7 | action_fallback BlogApp.Web.FallbackController 8 | 9 | def index(conn, _params) do 10 | posts = Blog.list_posts() 11 | render(conn, "index.json", posts: posts) 12 | end 13 | 14 | def create(conn, %{"post" => post_params}) do 15 | with {:ok, %Post{} = post} <- Blog.create_post(post_params) do 16 | conn 17 | |> put_status(:created) 18 | |> put_resp_header("location", post_path(conn, :show, post)) 19 | |> render("show.json", post: post) 20 | end 21 | end 22 | 23 | def show(conn, %{"id" => id}) do 24 | post = Blog.get_post!(id) 25 | render(conn, "show.json", post: post) 26 | end 27 | 28 | def update(conn, %{"id" => id, "post" => post_params}) do 29 | post = Blog.get_post!(id) 30 | 31 | with {:ok, %Post{} = post} <- Blog.update_post(post, post_params) do 32 | render(conn, "show.json", post: post) 33 | end 34 | end 35 | 36 | def delete(conn, %{"id" => id}) do 37 | post = Blog.get_post!(id) 38 | with {:ok, %Post{}} <- Blog.delete_post(post) do 39 | send_resp(conn, :no_content, "") 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /lib/blog_app/web/controllers/user_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule BlogApp.Web.UserController do 2 | use BlogApp.Web, :controller 3 | 4 | alias BlogApp.Accounts 5 | alias BlogApp.Accounts.User 6 | 7 | action_fallback BlogApp.Web.FallbackController 8 | 9 | def index(conn, _params) do 10 | users = Accounts.list_users() 11 | render(conn, "index.json", users: users) 12 | end 13 | 14 | def create(conn, %{"user" => user_params}) do 15 | with {:ok, %User{} = user} <- Accounts.create_user(user_params) do 16 | conn 17 | |> put_status(:created) 18 | |> put_resp_header("location", user_path(conn, :show, user)) 19 | |> render("show.json", user: user) 20 | end 21 | end 22 | 23 | def show(conn, %{"id" => id}) do 24 | user = Accounts.get_user!(id) 25 | render(conn, "show.json", user: user) 26 | end 27 | 28 | def update(conn, %{"id" => id, "user" => user_params}) do 29 | user = Accounts.get_user!(id) 30 | 31 | with {:ok, %User{} = user} <- Accounts.update_user(user, user_params) do 32 | render(conn, "show.json", user: user) 33 | end 34 | end 35 | 36 | def delete(conn, %{"id" => id}) do 37 | user = Accounts.get_user!(id) 38 | with {:ok, %User{}} <- Accounts.delete_user(user) do 39 | send_resp(conn, :no_content, "") 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /lib/blog_app/web/channels/user_socket.ex: -------------------------------------------------------------------------------- 1 | defmodule BlogApp.Web.UserSocket do 2 | use Phoenix.Socket 3 | 4 | ## Channels 5 | # channel "room:*", BlogApp.Web.RoomChannel 6 | 7 | ## Transports 8 | transport :websocket, Phoenix.Transports.WebSocket 9 | # transport :longpoll, Phoenix.Transports.LongPoll 10 | 11 | # Socket params are passed from the client and can 12 | # be used to verify and authenticate a user. After 13 | # verification, you can put default assigns into 14 | # the socket that will be set for all channels, ie 15 | # 16 | # {:ok, assign(socket, :user_id, verified_user_id)} 17 | # 18 | # To deny connection, return `:error`. 19 | # 20 | # See `Phoenix.Token` documentation for examples in 21 | # performing token verification on connect. 22 | def connect(_params, socket) do 23 | {:ok, socket} 24 | end 25 | 26 | # Socket id's are topics that allow you to identify all sockets for a given user: 27 | # 28 | # def id(socket), do: "user_socket:#{socket.assigns.user_id}" 29 | # 30 | # Would allow you to broadcast a "disconnect" event and terminate 31 | # all active sockets and channels for a given user: 32 | # 33 | # BlogApp.Web.Endpoint.broadcast("user_socket:#{user.id}", "disconnect", %{}) 34 | # 35 | # Returning `nil` makes this socket anonymous. 36 | def id(_socket), do: nil 37 | end 38 | -------------------------------------------------------------------------------- /test/support/data_case.ex: -------------------------------------------------------------------------------- 1 | defmodule BlogApp.DataCase do 2 | @moduledoc """ 3 | This module defines the setup for tests requiring 4 | access to the application's data layer. 5 | 6 | You may define functions here to be used as helpers in 7 | your tests. 8 | 9 | Finally, if the test case interacts with the database, 10 | it cannot be async. For this reason, every test runs 11 | inside a transaction which is reset at the beginning 12 | of the test unless the test case is marked as async. 13 | """ 14 | 15 | use ExUnit.CaseTemplate 16 | 17 | using do 18 | quote do 19 | alias BlogApp.Repo 20 | 21 | import Ecto 22 | import Ecto.Changeset 23 | import Ecto.Query 24 | import BlogApp.DataCase 25 | end 26 | end 27 | 28 | setup tags do 29 | :ok = Ecto.Adapters.SQL.Sandbox.checkout(BlogApp.Repo) 30 | 31 | unless tags[:async] do 32 | Ecto.Adapters.SQL.Sandbox.mode(BlogApp.Repo, {:shared, self()}) 33 | end 34 | 35 | :ok 36 | end 37 | 38 | @doc """ 39 | A helper that converts the changeset error messages 40 | for a given field into a list of strings for assertion: 41 | 42 | changeset = Blog.create_user(%{password: "short"}) 43 | assert "password is too short" in errors_on(changeset, :password) 44 | 45 | """ 46 | def errors_on(changeset, field) do 47 | for {message, opts} <- Keyword.get_values(changeset.errors, field) do 48 | Enum.reduce(opts, message, fn {key, value}, acc -> 49 | String.replace(acc, "%{#{key}}", to_string(value)) 50 | end) 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /config/dev.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | # For development, we disable any cache and enable 4 | # debugging and code reloading. 5 | # 6 | # The watchers configuration can be used to run external 7 | # watchers to your application. For example, we use it 8 | # with brunch.io to recompile .js and .css sources. 9 | config :blog_app, BlogApp.Web.Endpoint, 10 | http: [port: 4000], 11 | debug_errors: true, 12 | code_reloader: true, 13 | check_origin: false, 14 | watchers: [] 15 | 16 | # ## SSL Support 17 | # 18 | # In order to use HTTPS in development, a self-signed 19 | # certificate can be generated by running the following 20 | # command from your terminal: 21 | # 22 | # openssl req -new -newkey rsa:4096 -days 365 -nodes -x509 -subj "/C=US/ST=Denial/L=Springfield/O=Dis/CN=www.example.com" -keyout priv/server.key -out priv/server.pem 23 | # 24 | # The `http:` config above can be replaced with: 25 | # 26 | # https: [port: 4000, keyfile: "priv/server.key", certfile: "priv/server.pem"], 27 | # 28 | # If desired, both `http:` and `https:` keys can be 29 | # configured to run both http and https servers on 30 | # different ports. 31 | 32 | # Do not include metadata nor timestamps in development logs 33 | config :logger, :console, format: "[$level] $message\n" 34 | 35 | # Set a higher stacktrace during development. Avoid configuring such 36 | # in production as building large stacktraces may be expensive. 37 | config :phoenix, :stacktrace_depth, 20 38 | 39 | # Configure your database 40 | config :blog_app, BlogApp.Repo, 41 | adapter: Ecto.Adapters.Postgres, 42 | username: "postgres", 43 | password: "postgres", 44 | database: "blog_app_dev", 45 | hostname: "localhost", 46 | pool_size: 10 47 | -------------------------------------------------------------------------------- /lib/blog_app/web/web.ex: -------------------------------------------------------------------------------- 1 | defmodule BlogApp.Web do 2 | @moduledoc """ 3 | A module that keeps using definitions for controllers, 4 | views and so on. 5 | 6 | This can be used in your application as: 7 | 8 | use BlogApp.Web, :controller 9 | use BlogApp.Web, :view 10 | 11 | The definitions below will be executed for every view, 12 | controller, etc, so keep them short and clean, focused 13 | on imports, uses and aliases. 14 | 15 | Do NOT define functions inside the quoted expressions 16 | below. 17 | """ 18 | 19 | def controller do 20 | quote do 21 | use Phoenix.Controller, namespace: BlogApp.Web 22 | import Plug.Conn 23 | import BlogApp.Web.Router.Helpers 24 | import BlogApp.Web.Gettext 25 | end 26 | end 27 | 28 | def view do 29 | quote do 30 | use Phoenix.View, root: "lib/blog_app/web/templates", 31 | namespace: BlogApp.Web 32 | 33 | # Import convenience functions from controllers 34 | import Phoenix.Controller, only: [get_csrf_token: 0, get_flash: 2, view_module: 1] 35 | 36 | import BlogApp.Web.Router.Helpers 37 | import BlogApp.Web.ErrorHelpers 38 | import BlogApp.Web.Gettext 39 | end 40 | end 41 | 42 | def router do 43 | quote do 44 | use Phoenix.Router 45 | import Plug.Conn 46 | import Phoenix.Controller 47 | end 48 | end 49 | 50 | def channel do 51 | quote do 52 | use Phoenix.Channel 53 | import BlogApp.Web.Gettext 54 | end 55 | end 56 | 57 | @doc """ 58 | When used, dispatch to the appropriate controller/view/etc. 59 | """ 60 | defmacro __using__(which) when is_atom(which) do 61 | apply(__MODULE__, which, []) 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /lib/blog_app/web/endpoint.ex: -------------------------------------------------------------------------------- 1 | defmodule BlogApp.Web.Endpoint do 2 | use Phoenix.Endpoint, otp_app: :blog_app 3 | 4 | socket "/socket", BlogApp.Web.UserSocket 5 | 6 | # Serve at "/" the static files from "priv/static" directory. 7 | # 8 | # You should set gzip to true if you are running phoenix.digest 9 | # when deploying your static files in production. 10 | plug Plug.Static, 11 | at: "/", from: :blog_app, gzip: false, 12 | only: ~w(css fonts images js favicon.ico robots.txt) 13 | 14 | # Code reloading can be explicitly enabled under the 15 | # :code_reloader configuration of your endpoint. 16 | if code_reloading? do 17 | plug Phoenix.CodeReloader 18 | end 19 | 20 | plug Plug.RequestId 21 | plug Plug.Logger 22 | 23 | plug Plug.Parsers, 24 | parsers: [:urlencoded, :multipart, :json], 25 | pass: ["*/*"], 26 | json_decoder: Poison 27 | 28 | plug Plug.MethodOverride 29 | plug Plug.Head 30 | 31 | # The session will be stored in the cookie and signed, 32 | # this means its contents can be read but not tampered with. 33 | # Set :encryption_salt if you would also like to encrypt it. 34 | plug Plug.Session, 35 | store: :cookie, 36 | key: "_blog_app_key", 37 | signing_salt: "ZvwaEHVp" 38 | 39 | plug BlogApp.Web.Router 40 | 41 | @doc """ 42 | Dynamically loads configuration from the system environment 43 | on startup. 44 | 45 | It receives the endpoint configuration from the config files 46 | and must return the updated configuration. 47 | """ 48 | def load_from_system_env(config) do 49 | port = System.get_env("PORT") || raise "expected the PORT environment variable to be set" 50 | {:ok, Keyword.put(config, :http, [:inet6, port: port])} 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule BlogApp.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [app: :blog_app, 6 | version: "0.0.1", 7 | elixir: "~> 1.4", 8 | elixirc_paths: elixirc_paths(Mix.env), 9 | compilers: [:phoenix, :gettext] ++ Mix.compilers, 10 | build_embedded: Mix.env == :prod, 11 | start_permanent: Mix.env == :prod, 12 | aliases: aliases(), 13 | deps: deps()] 14 | end 15 | 16 | # Configuration for the OTP application. 17 | # 18 | # Type `mix help compile.app` for more information. 19 | def application do 20 | [mod: {BlogApp.Application, []}, 21 | extra_applications: [:logger]] 22 | end 23 | 24 | # Specifies which paths to compile per environment. 25 | defp elixirc_paths(:test), do: ["lib", "test/support"] 26 | defp elixirc_paths(_), do: ["lib"] 27 | 28 | # Specifies your project dependencies. 29 | # 30 | # Type `mix help deps` for examples and options. 31 | defp deps do 32 | [ 33 | {:phoenix, "~> 1.3.0-rc"}, 34 | {:phoenix_pubsub, "~> 1.0"}, 35 | {:phoenix_ecto, "~> 3.2"}, 36 | {:postgrex, ">= 0.0.0"}, 37 | {:gettext, "~> 0.11"}, 38 | {:cowboy, "~> 1.0"}, 39 | {:absinthe, "~> 1.3.0-rc.0"}, 40 | {:absinthe_plug, "~> 1.3.0-rc.0"}, 41 | {:absinthe_ecto, git: "https://github.com/absinthe-graphql/absinthe_ecto.git"}, 42 | {:faker, "~> 0.7"}, 43 | ] 44 | end 45 | 46 | # Aliases are shortcuts or tasks specific to the current project. 47 | # For example, to create, migrate and run the seeds file at once: 48 | # 49 | # $ mix ecto.setup 50 | # 51 | # See the documentation for `Mix` for more info on aliases. 52 | defp aliases do 53 | ["ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"], 54 | "ecto.reset": ["ecto.drop", "ecto.setup"], 55 | "test": ["ecto.create --quiet", "ecto.migrate", "test"]] 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /test/blog_test.exs: -------------------------------------------------------------------------------- 1 | defmodule BlogApp.BlogTest do 2 | use BlogApp.DataCase 3 | 4 | alias BlogApp.Blog 5 | alias BlogApp.Blog.Post 6 | 7 | @create_attrs %{body: "some body", title: "some title"} 8 | @update_attrs %{body: "some updated body", title: "some updated title"} 9 | @invalid_attrs %{body: nil, title: nil} 10 | 11 | def fixture(:post, attrs \\ @create_attrs) do 12 | {:ok, post} = Blog.create_post(attrs) 13 | post 14 | end 15 | 16 | test "list_posts/1 returns all posts" do 17 | post = fixture(:post) 18 | assert Blog.list_posts() == [post] 19 | end 20 | 21 | test "get_post! returns the post with given id" do 22 | post = fixture(:post) 23 | assert Blog.get_post!(post.id) == post 24 | end 25 | 26 | test "create_post/1 with valid data creates a post" do 27 | assert {:ok, %Post{} = post} = Blog.create_post(@create_attrs) 28 | assert post.body == "some body" 29 | assert post.title == "some title" 30 | end 31 | 32 | test "create_post/1 with invalid data returns error changeset" do 33 | assert {:error, %Ecto.Changeset{}} = Blog.create_post(@invalid_attrs) 34 | end 35 | 36 | test "update_post/2 with valid data updates the post" do 37 | post = fixture(:post) 38 | assert {:ok, post} = Blog.update_post(post, @update_attrs) 39 | assert %Post{} = post 40 | assert post.body == "some updated body" 41 | assert post.title == "some updated title" 42 | end 43 | 44 | test "update_post/2 with invalid data returns error changeset" do 45 | post = fixture(:post) 46 | assert {:error, %Ecto.Changeset{}} = Blog.update_post(post, @invalid_attrs) 47 | assert post == Blog.get_post!(post.id) 48 | end 49 | 50 | test "delete_post/1 deletes the post" do 51 | post = fixture(:post) 52 | assert {:ok, %Post{}} = Blog.delete_post(post) 53 | assert_raise Ecto.NoResultsError, fn -> Blog.get_post!(post.id) end 54 | end 55 | 56 | test "change_post/1 returns a post changeset" do 57 | post = fixture(:post) 58 | assert %Ecto.Changeset{} = Blog.change_post(post) 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /test/accounts_test.exs: -------------------------------------------------------------------------------- 1 | defmodule BlogApp.AccountsTest do 2 | use BlogApp.DataCase 3 | 4 | alias BlogApp.Accounts 5 | alias BlogApp.Accounts.User 6 | 7 | @create_attrs %{email: "some email", name: "some name"} 8 | @update_attrs %{email: "some updated email", name: "some updated name"} 9 | @invalid_attrs %{email: nil, name: nil} 10 | 11 | def fixture(:user, attrs \\ @create_attrs) do 12 | {:ok, user} = Accounts.create_user(attrs) 13 | user 14 | end 15 | 16 | test "list_users/1 returns all users" do 17 | user = fixture(:user) 18 | assert Accounts.list_users() == [user] 19 | end 20 | 21 | test "get_user! returns the user with given id" do 22 | user = fixture(:user) 23 | assert Accounts.get_user!(user.id) == user 24 | end 25 | 26 | test "create_user/1 with valid data creates a user" do 27 | assert {:ok, %User{} = user} = Accounts.create_user(@create_attrs) 28 | assert user.email == "some email" 29 | assert user.name == "some name" 30 | end 31 | 32 | test "create_user/1 with invalid data returns error changeset" do 33 | assert {:error, %Ecto.Changeset{}} = Accounts.create_user(@invalid_attrs) 34 | end 35 | 36 | test "update_user/2 with valid data updates the user" do 37 | user = fixture(:user) 38 | assert {:ok, user} = Accounts.update_user(user, @update_attrs) 39 | assert %User{} = user 40 | assert user.email == "some updated email" 41 | assert user.name == "some updated name" 42 | end 43 | 44 | test "update_user/2 with invalid data returns error changeset" do 45 | user = fixture(:user) 46 | assert {:error, %Ecto.Changeset{}} = Accounts.update_user(user, @invalid_attrs) 47 | assert user == Accounts.get_user!(user.id) 48 | end 49 | 50 | test "delete_user/1 deletes the user" do 51 | user = fixture(:user) 52 | assert {:ok, %User{}} = Accounts.delete_user(user) 53 | assert_raise Ecto.NoResultsError, fn -> Accounts.get_user!(user.id) end 54 | end 55 | 56 | test "change_user/1 returns a user changeset" do 57 | user = fixture(:user) 58 | assert %Ecto.Changeset{} = Accounts.change_user(user) 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /lib/blog_app/blog/blog.ex: -------------------------------------------------------------------------------- 1 | defmodule BlogApp.Blog do 2 | @moduledoc """ 3 | The boundary for the Blog system. 4 | """ 5 | 6 | import Ecto.{Query, Changeset}, warn: false 7 | alias BlogApp.Repo 8 | 9 | alias BlogApp.Blog.Post 10 | 11 | @doc """ 12 | Returns the list of posts. 13 | 14 | ## Examples 15 | 16 | iex> list_posts() 17 | [%Post{}, ...] 18 | 19 | """ 20 | def list_posts do 21 | Repo.all(Post) 22 | end 23 | 24 | @doc """ 25 | Gets a single post. 26 | 27 | Raises `Ecto.NoResultsError` if the Post does not exist. 28 | 29 | ## Examples 30 | 31 | iex> get_post!(123) 32 | %Post{} 33 | 34 | iex> get_post!(456) 35 | ** (Ecto.NoResultsError) 36 | 37 | """ 38 | def get_post!(id), do: Repo.get!(Post, id) 39 | 40 | @doc """ 41 | Creates a post. 42 | 43 | ## Examples 44 | 45 | iex> create_post(%{field: value}) 46 | {:ok, %Post{}} 47 | 48 | iex> create_post(%{field: bad_value}) 49 | {:error, %Ecto.Changeset{}} 50 | 51 | """ 52 | def create_post(attrs \\ %{}) do 53 | %Post{} 54 | |> post_changeset(attrs) 55 | |> Repo.insert() 56 | end 57 | 58 | @doc """ 59 | Updates a post. 60 | 61 | ## Examples 62 | 63 | iex> update_post(post, %{field: new_value}) 64 | {:ok, %Post{}} 65 | 66 | iex> update_post(post, %{field: bad_value}) 67 | {:error, %Ecto.Changeset{}} 68 | 69 | """ 70 | def update_post(%Post{} = post, attrs) do 71 | post 72 | |> post_changeset(attrs) 73 | |> Repo.update() 74 | end 75 | 76 | @doc """ 77 | Deletes a Post. 78 | 79 | ## Examples 80 | 81 | iex> delete_post(post) 82 | {:ok, %Post{}} 83 | 84 | iex> delete_post(post) 85 | {:error, %Ecto.Changeset{}} 86 | 87 | """ 88 | def delete_post(%Post{} = post) do 89 | Repo.delete(post) 90 | end 91 | 92 | @doc """ 93 | Returns an `%Ecto.Changeset{}` for tracking post changes. 94 | 95 | ## Examples 96 | 97 | iex> change_post(post) 98 | %Ecto.Changeset{source: %Post{}} 99 | 100 | """ 101 | def change_post(%Post{} = post) do 102 | post_changeset(post, %{}) 103 | end 104 | 105 | defp post_changeset(%Post{} = post, attrs) do 106 | post 107 | |> cast(attrs, [:title, :body]) 108 | |> validate_required([:title, :body]) 109 | end 110 | end 111 | -------------------------------------------------------------------------------- /lib/blog_app/accounts/accounts.ex: -------------------------------------------------------------------------------- 1 | defmodule BlogApp.Accounts do 2 | @moduledoc """ 3 | The boundary for the Accounts system. 4 | """ 5 | 6 | import Ecto.{Query, Changeset}, warn: false 7 | alias BlogApp.Repo 8 | 9 | alias BlogApp.Accounts.User 10 | 11 | @doc """ 12 | Returns the list of users. 13 | 14 | ## Examples 15 | 16 | iex> list_users() 17 | [%User{}, ...] 18 | 19 | """ 20 | def list_users do 21 | Repo.all(User) 22 | end 23 | 24 | @doc """ 25 | Gets a single user. 26 | 27 | Raises `Ecto.NoResultsError` if the User does not exist. 28 | 29 | ## Examples 30 | 31 | iex> get_user!(123) 32 | %User{} 33 | 34 | iex> get_user!(456) 35 | ** (Ecto.NoResultsError) 36 | 37 | """ 38 | def get_user!(id), do: Repo.get!(User, id) 39 | 40 | @doc """ 41 | Creates a user. 42 | 43 | ## Examples 44 | 45 | iex> create_user(%{field: value}) 46 | {:ok, %User{}} 47 | 48 | iex> create_user(%{field: bad_value}) 49 | {:error, %Ecto.Changeset{}} 50 | 51 | """ 52 | def create_user(attrs \\ %{}) do 53 | %User{} 54 | |> user_changeset(attrs) 55 | |> Repo.insert() 56 | end 57 | 58 | @doc """ 59 | Updates a user. 60 | 61 | ## Examples 62 | 63 | iex> update_user(user, %{field: new_value}) 64 | {:ok, %User{}} 65 | 66 | iex> update_user(user, %{field: bad_value}) 67 | {:error, %Ecto.Changeset{}} 68 | 69 | """ 70 | def update_user(%User{} = user, attrs) do 71 | user 72 | |> user_changeset(attrs) 73 | |> Repo.update() 74 | end 75 | 76 | @doc """ 77 | Deletes a User. 78 | 79 | ## Examples 80 | 81 | iex> delete_user(user) 82 | {:ok, %User{}} 83 | 84 | iex> delete_user(user) 85 | {:error, %Ecto.Changeset{}} 86 | 87 | """ 88 | def delete_user(%User{} = user) do 89 | Repo.delete(user) 90 | end 91 | 92 | @doc """ 93 | Returns an `%Ecto.Changeset{}` for tracking user changes. 94 | 95 | ## Examples 96 | 97 | iex> change_user(user) 98 | %Ecto.Changeset{source: %User{}} 99 | 100 | """ 101 | def change_user(%User{} = user) do 102 | user_changeset(user, %{}) 103 | end 104 | 105 | defp user_changeset(%User{} = user, attrs) do 106 | user 107 | |> cast(attrs, [:name, :email]) 108 | |> validate_required([:name, :email]) 109 | end 110 | end 111 | -------------------------------------------------------------------------------- /config/prod.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | # For production, we often load configuration from external 4 | # sources, such as your system environment. For this reason, 5 | # you won't find the :http configuration below, but set inside 6 | # BlogApp.Web.Endpoint.load_from_system_env/1 dynamically. 7 | # Any dynamic configuration should be moved to such function. 8 | # 9 | # Don't forget to configure the url host to something meaningful, 10 | # Phoenix uses this information when generating URLs. 11 | # 12 | # Finally, we also include the path to a cache manifest 13 | # containing the digested version of static files. This 14 | # manifest is generated by the mix phoenix.digest task 15 | # which you typically run after static files are built. 16 | config :blog_app, BlogApp.Web.Endpoint, 17 | on_init: {Demo.Web.Endpoint, :load_from_system_env, []}, 18 | url: [host: "example.com", port: 80], 19 | cache_static_manifest: "priv/static/cache_manifest.json" 20 | 21 | # Do not print debug messages in production 22 | config :logger, level: :info 23 | 24 | # ## SSL Support 25 | # 26 | # To get SSL working, you will need to add the `https` key 27 | # to the previous section and set your `:url` port to 443: 28 | # 29 | # config :blog_app, BlogApp.Web.Endpoint, 30 | # ... 31 | # url: [host: "example.com", port: 443], 32 | # https: [:inet6, 33 | # port: 443, 34 | # keyfile: System.get_env("SOME_APP_SSL_KEY_PATH"), 35 | # certfile: System.get_env("SOME_APP_SSL_CERT_PATH")] 36 | # 37 | # Where those two env variables return an absolute path to 38 | # the key and cert in disk or a relative path inside priv, 39 | # for example "priv/ssl/server.key". 40 | # 41 | # We also recommend setting `force_ssl`, ensuring no data is 42 | # ever sent via http, always redirecting to https: 43 | # 44 | # config :blog_app, BlogApp.Web.Endpoint, 45 | # force_ssl: [hsts: true] 46 | # 47 | # Check `Plug.SSL` for all available options in `force_ssl`. 48 | 49 | # ## Using releases 50 | # 51 | # If you are doing OTP releases, you need to instruct Phoenix 52 | # to start the server for all endpoints: 53 | # 54 | # config :phoenix, :serve_endpoints, true 55 | # 56 | # Alternatively, you can configure exactly which server to 57 | # start per endpoint: 58 | # 59 | # config :blog_app, BlogApp.Web.Endpoint, server: true 60 | # 61 | 62 | # Finally import the config/prod.secret.exs 63 | # which should be versioned separately. 64 | import_config "prod.secret.exs" 65 | -------------------------------------------------------------------------------- /test/web/controllers/post_controller_test.exs: -------------------------------------------------------------------------------- 1 | defmodule BlogApp.Web.PostControllerTest do 2 | use BlogApp.Web.ConnCase 3 | 4 | alias BlogApp.Blog 5 | alias BlogApp.Blog.Post 6 | 7 | @create_attrs %{body: "some body", title: "some title"} 8 | @update_attrs %{body: "some updated body", title: "some updated title"} 9 | @invalid_attrs %{body: nil, title: nil} 10 | 11 | def fixture(:post) do 12 | {:ok, post} = Blog.create_post(@create_attrs) 13 | post 14 | end 15 | 16 | setup %{conn: conn} do 17 | {:ok, conn: put_req_header(conn, "accept", "application/json")} 18 | end 19 | 20 | test "lists all entries on index", %{conn: conn} do 21 | conn = get conn, post_path(conn, :index) 22 | assert json_response(conn, 200)["data"] == [] 23 | end 24 | 25 | test "creates post and renders post when data is valid", %{conn: conn} do 26 | conn = post conn, post_path(conn, :create), post: @create_attrs 27 | assert %{"id" => id} = json_response(conn, 201)["data"] 28 | 29 | conn = get conn, post_path(conn, :show, id) 30 | assert json_response(conn, 200)["data"] == %{ 31 | "id" => id, 32 | "body" => "some body", 33 | "title" => "some title"} 34 | end 35 | 36 | test "does not create post and renders errors when data is invalid", %{conn: conn} do 37 | conn = post conn, post_path(conn, :create), post: @invalid_attrs 38 | assert json_response(conn, 422)["errors"] != %{} 39 | end 40 | 41 | test "updates chosen post and renders post when data is valid", %{conn: conn} do 42 | %Post{id: id} = post = fixture(:post) 43 | conn = put conn, post_path(conn, :update, post), post: @update_attrs 44 | assert %{"id" => ^id} = json_response(conn, 200)["data"] 45 | 46 | conn = get conn, post_path(conn, :show, id) 47 | assert json_response(conn, 200)["data"] == %{ 48 | "id" => id, 49 | "body" => "some updated body", 50 | "title" => "some updated title"} 51 | end 52 | 53 | test "does not update chosen post and renders errors when data is invalid", %{conn: conn} do 54 | post = fixture(:post) 55 | conn = put conn, post_path(conn, :update, post), post: @invalid_attrs 56 | assert json_response(conn, 422)["errors"] != %{} 57 | end 58 | 59 | test "deletes chosen post", %{conn: conn} do 60 | post = fixture(:post) 61 | conn = delete conn, post_path(conn, :delete, post) 62 | assert response(conn, 204) 63 | assert_error_sent 404, fn -> 64 | get conn, post_path(conn, :show, post) 65 | end 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /test/web/controllers/user_controller_test.exs: -------------------------------------------------------------------------------- 1 | defmodule BlogApp.Web.UserControllerTest do 2 | use BlogApp.Web.ConnCase 3 | 4 | alias BlogApp.Accounts 5 | alias BlogApp.Accounts.User 6 | 7 | @create_attrs %{email: "some email", name: "some name"} 8 | @update_attrs %{email: "some updated email", name: "some updated name"} 9 | @invalid_attrs %{email: nil, name: nil} 10 | 11 | def fixture(:user) do 12 | {:ok, user} = Accounts.create_user(@create_attrs) 13 | user 14 | end 15 | 16 | setup %{conn: conn} do 17 | {:ok, conn: put_req_header(conn, "accept", "application/json")} 18 | end 19 | 20 | test "lists all entries on index", %{conn: conn} do 21 | conn = get conn, user_path(conn, :index) 22 | assert json_response(conn, 200)["data"] == [] 23 | end 24 | 25 | test "creates user and renders user when data is valid", %{conn: conn} do 26 | conn = post conn, user_path(conn, :create), user: @create_attrs 27 | assert %{"id" => id} = json_response(conn, 201)["data"] 28 | 29 | conn = get conn, user_path(conn, :show, id) 30 | assert json_response(conn, 200)["data"] == %{ 31 | "id" => id, 32 | "email" => "some email", 33 | "name" => "some name"} 34 | end 35 | 36 | test "does not create user and renders errors when data is invalid", %{conn: conn} do 37 | conn = post conn, user_path(conn, :create), user: @invalid_attrs 38 | assert json_response(conn, 422)["errors"] != %{} 39 | end 40 | 41 | test "updates chosen user and renders user when data is valid", %{conn: conn} do 42 | %User{id: id} = user = fixture(:user) 43 | conn = put conn, user_path(conn, :update, user), user: @update_attrs 44 | assert %{"id" => ^id} = json_response(conn, 200)["data"] 45 | 46 | conn = get conn, user_path(conn, :show, id) 47 | assert json_response(conn, 200)["data"] == %{ 48 | "id" => id, 49 | "email" => "some updated email", 50 | "name" => "some updated name"} 51 | end 52 | 53 | test "does not update chosen user and renders errors when data is invalid", %{conn: conn} do 54 | user = fixture(:user) 55 | conn = put conn, user_path(conn, :update, user), user: @invalid_attrs 56 | assert json_response(conn, 422)["errors"] != %{} 57 | end 58 | 59 | test "deletes chosen user", %{conn: conn} do 60 | user = fixture(:user) 61 | conn = delete conn, user_path(conn, :delete, user) 62 | assert response(conn, 204) 63 | assert_error_sent 404, fn -> 64 | get conn, user_path(conn, :show, user) 65 | end 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /priv/gettext/en/LC_MESSAGES/errors.po: -------------------------------------------------------------------------------- 1 | ## `msgid`s in this file come from POT (.pot) files. 2 | ## 3 | ## Do not add, change, or remove `msgid`s manually here as 4 | ## they're tied to the ones in the corresponding POT file 5 | ## (with the same domain). 6 | ## 7 | ## Use `mix gettext.extract --merge` or `mix gettext.merge` 8 | ## to merge POT files into PO files. 9 | msgid "" 10 | msgstr "" 11 | "Language: en\n" 12 | 13 | ## From Ecto.Changeset.cast/4 14 | msgid "can't be blank" 15 | msgstr "" 16 | 17 | ## From Ecto.Changeset.unique_constraint/3 18 | msgid "has already been taken" 19 | msgstr "" 20 | 21 | ## From Ecto.Changeset.put_change/3 22 | msgid "is invalid" 23 | msgstr "" 24 | 25 | ## From Ecto.Changeset.validate_format/3 26 | msgid "has invalid format" 27 | msgstr "" 28 | 29 | ## From Ecto.Changeset.validate_subset/3 30 | msgid "has an invalid entry" 31 | msgstr "" 32 | 33 | ## From Ecto.Changeset.validate_exclusion/3 34 | msgid "is reserved" 35 | msgstr "" 36 | 37 | ## From Ecto.Changeset.validate_confirmation/3 38 | msgid "does not match confirmation" 39 | msgstr "" 40 | 41 | ## From Ecto.Changeset.no_assoc_constraint/3 42 | msgid "is still associated with this entry" 43 | msgstr "" 44 | 45 | msgid "are still associated with this entry" 46 | msgstr "" 47 | 48 | ## From Ecto.Changeset.validate_length/3 49 | msgid "should be %{count} character(s)" 50 | msgid_plural "should be %{count} character(s)" 51 | msgstr[0] "" 52 | msgstr[1] "" 53 | 54 | msgid "should have %{count} item(s)" 55 | msgid_plural "should have %{count} item(s)" 56 | msgstr[0] "" 57 | msgstr[1] "" 58 | 59 | msgid "should be at least %{count} character(s)" 60 | msgid_plural "should be at least %{count} character(s)" 61 | msgstr[0] "" 62 | msgstr[1] "" 63 | 64 | msgid "should have at least %{count} item(s)" 65 | msgid_plural "should have at least %{count} item(s)" 66 | msgstr[0] "" 67 | msgstr[1] "" 68 | 69 | msgid "should be at most %{count} character(s)" 70 | msgid_plural "should be at most %{count} character(s)" 71 | msgstr[0] "" 72 | msgstr[1] "" 73 | 74 | msgid "should have at most %{count} item(s)" 75 | msgid_plural "should have at most %{count} item(s)" 76 | msgstr[0] "" 77 | msgstr[1] "" 78 | 79 | ## From Ecto.Changeset.validate_number/3 80 | msgid "must be less than %{number}" 81 | msgstr "" 82 | 83 | msgid "must be greater than %{number}" 84 | msgstr "" 85 | 86 | msgid "must be less than or equal to %{number}" 87 | msgstr "" 88 | 89 | msgid "must be greater than or equal to %{number}" 90 | msgstr "" 91 | 92 | msgid "must be equal to %{number}" 93 | msgstr "" 94 | -------------------------------------------------------------------------------- /priv/gettext/errors.pot: -------------------------------------------------------------------------------- 1 | ## This file is a PO Template file. 2 | ## 3 | ## `msgid`s here are often extracted from source code. 4 | ## Add new translations manually only if they're dynamic 5 | ## translations that can't be statically extracted. 6 | ## 7 | ## Run `mix gettext.extract` to bring this file up to 8 | ## date. Leave `msgstr`s empty as changing them here as no 9 | ## effect: edit them in PO (`.po`) files instead. 10 | 11 | ## From Ecto.Changeset.cast/4 12 | msgid "can't be blank" 13 | msgstr "" 14 | 15 | ## From Ecto.Changeset.unique_constraint/3 16 | msgid "has already been taken" 17 | msgstr "" 18 | 19 | ## From Ecto.Changeset.put_change/3 20 | msgid "is invalid" 21 | msgstr "" 22 | 23 | ## From Ecto.Changeset.validate_format/3 24 | msgid "has invalid format" 25 | msgstr "" 26 | 27 | ## From Ecto.Changeset.validate_subset/3 28 | msgid "has an invalid entry" 29 | msgstr "" 30 | 31 | ## From Ecto.Changeset.validate_exclusion/3 32 | msgid "is reserved" 33 | msgstr "" 34 | 35 | ## From Ecto.Changeset.validate_confirmation/3 36 | msgid "does not match confirmation" 37 | msgstr "" 38 | 39 | ## From Ecto.Changeset.no_assoc_constraint/3 40 | msgid "is still associated with this entry" 41 | msgstr "" 42 | 43 | msgid "are still associated with this entry" 44 | msgstr "" 45 | 46 | ## From Ecto.Changeset.validate_length/3 47 | msgid "should be %{count} character(s)" 48 | msgid_plural "should be %{count} character(s)" 49 | msgstr[0] "" 50 | msgstr[1] "" 51 | 52 | msgid "should have %{count} item(s)" 53 | msgid_plural "should have %{count} item(s)" 54 | msgstr[0] "" 55 | msgstr[1] "" 56 | 57 | msgid "should be at least %{count} character(s)" 58 | msgid_plural "should be at least %{count} character(s)" 59 | msgstr[0] "" 60 | msgstr[1] "" 61 | 62 | msgid "should have at least %{count} item(s)" 63 | msgid_plural "should have at least %{count} item(s)" 64 | msgstr[0] "" 65 | msgstr[1] "" 66 | 67 | msgid "should be at most %{count} character(s)" 68 | msgid_plural "should be at most %{count} character(s)" 69 | msgstr[0] "" 70 | msgstr[1] "" 71 | 72 | msgid "should have at most %{count} item(s)" 73 | msgid_plural "should have at most %{count} item(s)" 74 | msgstr[0] "" 75 | msgstr[1] "" 76 | 77 | ## From Ecto.Changeset.validate_number/3 78 | msgid "must be less than %{number}" 79 | msgstr "" 80 | 81 | msgid "must be greater than %{number}" 82 | msgstr "" 83 | 84 | msgid "must be less than or equal to %{number}" 85 | msgstr "" 86 | 87 | msgid "must be greater than or equal to %{number}" 88 | msgstr "" 89 | 90 | msgid "must be equal to %{number}" 91 | msgstr "" 92 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{"absinthe": {:hex, :absinthe, "1.3.0-rc.0", "99c4557fd10fc1e7d8476cc49444974d9a7e13d02bbe6a7de434513b9df55fcb", [:mix], []}, 2 | "absinthe_ecto": {:git, "https://github.com/absinthe-graphql/absinthe_ecto.git", "4fda32258875b6122faf6ca0f7c92ce72fc55012", []}, 3 | "absinthe_plug": {:hex, :absinthe_plug, "1.3.0-rc.1", "01e55ba1287e969535672e2542df03ff0dc680c1f705b0848775f9c2907835e6", [:mix], [{:absinthe, "~> 1.3.0-rc.0", [hex: :absinthe, optional: false]}, {:plug, "~> 1.3.2 or ~> 1.4", [hex: :plug, optional: false]}]}, 4 | "connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [:mix], []}, 5 | "cowboy": {:hex, :cowboy, "1.1.2", "61ac29ea970389a88eca5a65601460162d370a70018afe6f949a29dca91f3bb0", [:rebar3], [{:cowlib, "~> 1.0.2", [hex: :cowlib, optional: false]}, {:ranch, "~> 1.3.2", [hex: :ranch, optional: false]}]}, 6 | "cowlib": {:hex, :cowlib, "1.0.2", "9d769a1d062c9c3ac753096f868ca121e2730b9a377de23dec0f7e08b1df84ee", [:make], []}, 7 | "db_connection": {:hex, :db_connection, "1.1.2", "2865c2a4bae0714e2213a0ce60a1b12d76a6efba0c51fbda59c9ab8d1accc7a8", [:mix], [{:connection, "~> 1.0.2", [hex: :connection, optional: false]}, {:poolboy, "~> 1.5", [hex: :poolboy, optional: true]}, {:sbroker, "~> 1.0", [hex: :sbroker, optional: true]}]}, 8 | "decimal": {:hex, :decimal, "1.3.1", "157b3cedb2bfcb5359372a7766dd7a41091ad34578296e951f58a946fcab49c6", [:mix], []}, 9 | "ecto": {:hex, :ecto, "2.1.4", "d1ba932813ec0e0d9db481ef2c17777f1cefb11fc90fa7c142ff354972dfba7e", [:mix], [{:db_connection, "~> 1.1", [hex: :db_connection, optional: true]}, {:decimal, "~> 1.2", [hex: :decimal, optional: false]}, {:mariaex, "~> 0.8.0", [hex: :mariaex, optional: true]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, optional: true]}, {:poolboy, "~> 1.5", [hex: :poolboy, optional: false]}, {:postgrex, "~> 0.13.0", [hex: :postgrex, optional: true]}, {:sbroker, "~> 1.0", [hex: :sbroker, optional: true]}]}, 10 | "faker": {:hex, :faker, "0.7.0", "2c42deeac7be717173c78c77fb3edc749fb5d5e460e33d01fe592ae99acc2f0d", [:mix], []}, 11 | "gettext": {:hex, :gettext, "0.13.1", "5e0daf4e7636d771c4c71ad5f3f53ba09a9ae5c250e1ab9c42ba9edccc476263", [:mix], []}, 12 | "mime": {:hex, :mime, "1.1.0", "01c1d6f4083d8aa5c7b8c246ade95139620ef8effb009edde934e0ec3b28090a", [:mix], []}, 13 | "phoenix": {:hex, :phoenix, "1.3.0-rc.1", "0d04948a4bd24823f101024c07b6a4d35e58f1fd92a465c1bc75dd37acd1041a", [:mix], [{:cowboy, "~> 1.0", [hex: :cowboy, optional: true]}, {:phoenix_pubsub, "~> 1.0", [hex: :phoenix_pubsub, optional: false]}, {:plug, "~> 1.3.2 or ~> 1.4", [hex: :plug, optional: false]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, optional: false]}]}, 14 | "phoenix_ecto": {:hex, :phoenix_ecto, "3.2.3", "450c749876ff1de4a78fdb305a142a76817c77a1cd79aeca29e5fc9a6c630b26", [:mix], [{:ecto, "~> 2.1", [hex: :ecto, optional: false]}, {:phoenix_html, "~> 2.9", [hex: :phoenix_html, optional: true]}, {:plug, "~> 1.0", [hex: :plug, optional: false]}]}, 15 | "phoenix_pubsub": {:hex, :phoenix_pubsub, "1.0.1", "c10ddf6237007c804bf2b8f3c4d5b99009b42eca3a0dfac04ea2d8001186056a", [:mix], []}, 16 | "plug": {:hex, :plug, "1.3.5", "7503bfcd7091df2a9761ef8cecea666d1f2cc454cbbaf0afa0b6e259203b7031", [:mix], [{:cowboy, "~> 1.0.1 or ~> 1.1", [hex: :cowboy, optional: true]}, {:mime, "~> 1.0", [hex: :mime, optional: false]}]}, 17 | "poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], []}, 18 | "poolboy": {:hex, :poolboy, "1.5.1", "6b46163901cfd0a1b43d692657ed9d7e599853b3b21b95ae5ae0a777cf9b6ca8", [:rebar], []}, 19 | "postgrex": {:hex, :postgrex, "0.13.2", "2b88168fc6a5456a27bfb54ccf0ba4025d274841a7a3af5e5deb1b755d95154e", [:mix], [{:connection, "~> 1.0", [hex: :connection, optional: false]}, {:db_connection, "~> 1.1", [hex: :db_connection, optional: false]}, {:decimal, "~> 1.0", [hex: :decimal, optional: false]}]}, 20 | "ranch": {:hex, :ranch, "1.3.2", "e4965a144dc9fbe70e5c077c65e73c57165416a901bd02ea899cfd95aa890986", [:rebar3], []}} 21 | --------------------------------------------------------------------------------