Articles

Publishing an article

The API spec for creating an article is as follows:

HTTP verb URL Required fields
POST /api/articles title, description, body

Example request body:

{
  "article": {
    "title": "How to train your dragon",
    "description": "Ever wonder how?",
    "body": "You have to believe",
    "tagList": ["dragons", "training"]
  }
}

Example response body:

{
  "article": {
    "slug": "how-to-train-your-dragon",
    "title": "How to train your dragon",
    "description": "Ever wonder how?",
    "body": "You have to believe",
    "tagList": ["dragons", "training"],
    "createdAt": "2016-02-18T03:22:56.637Z",
    "updatedAt": "2016-02-18T03:48:35.824Z",
    "favorited": false,
    "favoritesCount": 0,
    "author": {
      "username": "jake",
      "bio": "I work at statefarm",
      "image": "https://i.stack.imgur.com/xHWG8.jpg",
      "following": false
    }
  }
}

We’ll use the phx.gen.json generator once again to create a new context for articles. The generator will create the blog context, schema, controller, and JSON view. We already know which fields we need for articles so we can include them, with their types, in the generator command:

$ mix phx.gen.json Blog Article articles slug:text title:text description:text body:text tag_list:array:te\
xt favorite_count:integer published_at:naive_datetime author_uuid:binary author_username:text author_bio:t\
ext author_image:text --table blog_articles

Overall, this generator will add the following files to lib/conduit:

  • Context module in lib/conduit/blog/blog.ex, serving as the public API boundary.
  • Ecto schema in lib/conduit/blog/article.ex, and a database migration to create the blog_articles table.
  • View in lib/conduit_web/views/article_view.ex.
  • Controller in lib/conduit_web/controllers/article_controller.ex.
  • Unit and integration tests in test/conduit/blog.

The only change to the generated module locations is to move the article Ecto schema into the lib/conduit/blog/projections folder, not the blog context root.

Authoring articles

Before we begin publishing articles we’ll take a small detour since we first need a way of identifying their author. We could just use our existing user aggregate and read model projection as a convenience. However, we’ve already determined that accounts and blog are separate contexts, therefore they shouldn’t necessarily share models.

Instead, we’re going to model authors as part of the blog context, segregated from users, but have them related by their identity. There will be a one-to-one mapping from user accounts to blog authors. A benefit of this separation is that the user and author models are only responsible for concerns related to their own role; the user model deals with a user’s email and password, whereas the author model will contain their bio, profile image, and can be used for tracking followers.

We’ll need to build an author aggregate, create author command and author created domain event, and use a Commanded event handler to create an author whenever a user is registered. But first let’s define an integration test that verifies an author is created after successful user registration.

The following integration test uses the assert_receive_event helper function from Commanded’s event assertions module. Here we assert that an AuthorCreated domain event is created at some point after user registration and verify it’s for the same user:

# test/conduit/blog/author_test.exs
defmodule Conduit.Blog.AuthorTest do
  use Conduit.DataCase

  import Commanded.Assertions.EventAssertions

  alias Conduit.Accounts
  alias Conduit.Accounts.Projections.User
  alias Conduit.Blog.Events.AuthorCreated

  describe "an author" do
    @tag :integration
    test "should be created when user registered" do
      assert {:ok, %User{} = user} = Accounts.register_user(build(:user))

      assert_receive_event AuthorCreated, fn event ->
        assert event.user_uuid == user.uuid
        assert event.username == user.username
      end
    end
  end
end

As mentioned above we’ll use an event handler to create the author whenever a UserRegistered event occurs. An event handler is used whenever you need to react to a domain event being created. It’s a good extension point to use for adding auxiliary concerns and integrating separate contexts.

The handler below will delegate to a create_author/1 function we will define in the new blog context. Since the handler is modelling a business process I’ve defined it in a workflows folder within the blog context and have named it after its behaviour:

# lib/conduit/blog/workflows/create_author_from_user.ex
defmodule Conduit.Blog.Workflows.CreateAuthorFromUser do
  use Commanded.Event.Handler,
    name: "Blog.Workflows.CreateAuthorFromUser",
    consistency: :strong

  alias Conduit.Accounts.Events.UserRegistered
  alias Conduit.Blog

  def handle(%UserRegistered{user_uuid: user_uuid, username: username}, _metadata) do
    with {:ok, _author} <- Blog.create_author(%{user_uuid: user_uuid, username: username}) do
      :ok
    else
      reply -> reply
    end
  end
end

In the blog context we add the new create_author/1 function to dispatch a CreateAuthor command. It has a reference to the associated user aggregate by uuid and also includes the username:

# lib/conduit/blog/blog.ex
defmodule Conduit.Blog do
  @moduledoc """
  The boundary for the Blog system.
  """

  import Ecto.Query, warn: false

  alias Conduit.Blog.Projections.Article
  alias Conduit.Blog.Commands.CreateAuthor
  alias Conduit.Blog.Projections.Author
  alias Conduit.{Repo,Router}

  @doc """
  Create an author.
  An author shares the same uuid as the user, but with a different prefix.
  """
  def create_author(%{user_uuid: uuid} = attrs) do
    create_author =
      attrs
      |> CreateAuthor.new()
      |> CreateAuthor.assign_uuid(uuid)

    with :ok <- Router.dispatch(create_author, consistency: :strong) do
      get(Author, uuid)
    else
      reply -> reply
    end
  end

  defp get(schema, uuid) do
    case Repo.get(schema, uuid) do
      nil -> {:error, :not_found}
      projection -> {:ok, projection}
    end
  end
end

The CreateAuthor command contains the author identity, associated user identity, and username fields:

# lib/conduit/blog/commands/create_author.ex
defmodule Conduit.Blog.Commands.CreateAuthor do
  defstruct [
    author_uuid: "",
    user_uuid: "",
    username: "",
  ]

  use ExConstructor
  use Vex.Struct

  alias Conduit.Blog.Commands.CreateAuthor

  validates :author_uuid, uuid: true

  validates :user_uuid, uuid: true

  validates :username,
    presence: [message: "can't be empty"],
    format: [with: ~r/^[a-z0-9]+$/, allow_nil: true, allow_blank: true, message: "is invalid"],
    string: true

  @doc """
  Assign a unique identity
  """
  def assign_uuid(%CreateAuthor{} = create_author, uuid) do
    %CreateAuthor{create_author | author_uuid: uuid}
  end
end

We use Commanded’s identity prefix feature to allow the user and author aggregates to share the same aggregate identity. In our router module we identify both aggregates by their respective field (author_uuid or user_uuid) and also provide the prefix option used to differentiate between the event streams used to store their domain events. Author aggregates are prefixed with “author-“ (e.g. author-53db6101-6725-4332-ba94-75b4d05213ab) and users by “user-“ (e.g. user-53db6101-6725-4332-ba94-75b4d05213ab). This allows an easy way of correlating an author with its associated user account, and vice versa.

# lib/conduit/router.ex
defmodule Conduit.Router do
  use Commanded.Commands.Router

  alias Conduit.Accounts.Aggregates.User
  alias Conduit.Accounts.Commands.RegisterUser
  alias Conduit.Blog.Aggregates.Author
  alias Conduit.Blog.Commands.CreateAuthor
  alias Conduit.Support.Middleware.{Uniqueness,Validate}

  middleware Validate
  middleware Uniqueness

  identify Author, by: :author_uuid, prefix: "author-"
  identify User, by: :user_uuid, prefix: "user-"

  dispatch [CreateAuthor], to: Author
  dispatch [RegisterUser], to: User
end

The author aggregate has a single execute/2 function to create an instance, returning an AuthorCreated event.

# lib/conduit/blog/aggregates/author.ex
defmodule Conduit.Blog.Aggregates.Author do
  defstruct [
    :uuid,
    :user_uuid,
    :username,
    :bio,
    :image,
  ]

  alias Conduit.Blog.Aggregates.Author
  alias Conduit.Blog.Commands.CreateAuthor
  alias Conduit.Blog.Events.AuthorCreated

  @doc """
  Creates an author
  """
  def execute(%Author{uuid: nil}, %CreateAuthor{} = create) do
    %AuthorCreated{
      author_uuid: create.author_uuid,
      user_uuid: create.user_uuid,
      username: create.username,
    }
  end

  # state mutators

  def apply(%Author{} = author, %AuthorCreated{} = created) do
    %Author{author |
      uuid: created.author_uuid,
      user_uuid: created.user_uuid,
      username: created.username,
    }
  end
end

We define an author projection (Ecto schema) and a migration to create the blog_authors table. The corresponding projector module is shown below:

# lib/conduit/blog/projectors/article.ex
defmodule Conduit.Blog.Projectors.Article do
  use Commanded.Projections.Ecto,
    name: "Blog.Projectors.Article",
    consistency: :strong

  alias Conduit.Blog.Events.AuthorCreated
  alias Conduit.Blog.Projections.Author
  alias Conduit.Repo

  project %AuthorCreated{} = author do
    Ecto.Multi.insert(multi, :author, %Author{
      uuid: author.author_uuid,
      user_uuid: author.user_uuid,
      username: author.username,
      bio: nil,
      image: nil,
    })
  end
end

The projector is named article projector as this will be used for projecting both authors and their published articles. We’ll see later why two projections are built using a single projector; it’s because we need to query an article’s author to copy their details into the article projection during publishing.

Finally, the article projector and create author workflow are included as supervised processes in a new blog supervisor:

# lib/conduit/blog/supervisor.ex
defmodule Conduit.Blog.Supervisor do
  use Supervisor

  alias Conduit.Blog

  def start_link do
    Supervisor.start_link(__MODULE__, [], name: __MODULE__)
  end

  def init(_arg) do
    Supervisor.init([
      Blog.Projectors.Article,
      Blog.Workflows.CreateAuthorFromUser,
    ], strategy: :one_for_one)
  end
end

This supervisor is then added to the top level application supervisor in lib/conduit/application.ex.

With this chunk of work done we can execute our initial test to verify an author is successfully created in response to registering a user:

$ mix test test/conduit/blog/author_test.exs

By now you should be familiar with the test-driven development cycle we are following:

  1. Write failing integration tests.
  2. Build a web controller and define the public API required for the context.
  3. Implement the context public API, returning empty data.
  4. Write failing unit tests for the context.
  5. Build domain model (aggregate, commands, and events) to fulfil the context behaviour.
  6. Verify unit tests and integration tests pass.

This outside in approach helps to define the outcome we’re working towards in the integration test. Then guides us, step by step, to build the supporting code moving towards that goal.

Publish article integration test

We’ll begin with a controller integration test for the “happy path” of successfully publishing an article. A POST request to /api/articles should return a 201 response code with the article as JSON data:

# test/conduit_web/controllers/article_controller_test.exs
defmodule ConduitWeb.ArticleControllerTest do
  use ConduitWeb.ConnCase

  setup %{conn: conn} do
    {:ok, conn: put_req_header(conn, "accept", "application/json")}
  end

  describe "publish article" do
    @tag :web
    test "should create and return article when data is valid", %{conn: conn} do
      conn = post authenticated_conn(conn), article_path(conn, :create), article: build(:article)
      json = json_response(conn, 201)["article"]
      created_at = json["createdAt"]
      updated_at = json["updatedAt"]

      assert json == %{
        "slug" => "how-to-train-your-dragon",
        "title" => "How to train your dragon",
        "description" => "Ever wonder how?",
        "body" => "You have to believe",
        "tagList" => ["dragons", "training"],
        "createdAt" => created_at,
        "updatedAt" => updated_at,
        "favorited" => false,
        "favoritesCount" => 0,
        "author" => %{
          "username" => "jake",
          "bio" => nil,
          "image" => nil,
          "following" => false,
        }
      }
      refute created_at == ""
      refute updated_at == ""
    end
  end
end

The favorited and favoritesCount won’t be supported just yet, so we will fake it, until we make it and just return false and 0 respectively. We will return to build this functionality when we add the favourite articles feature.

Our test requires a new factory function, in test/support/factory.ex, to build the parameters for an article:

defmodule Conduit.Factory do
  use ExMachina

  def article_factory do
    %{
      slug: "how-to-train-your-dragon",
      title: "How to train your dragon",
      description: "Ever wonder how?",
      body: "You have to believe",
      tag_list: ["dragons", "training"],
      author_uuid: UUID.uuid4(),
    }
  end
end

The web controller test also makes use of a convenience function, authenticated_conn/1, to register a user and set their JWT token. This register a new user account and authenticates the request to be sent to the controller as the newly registered user:

# test/support/conn_helpers.ex
defmodule ConduitWeb.ConnHelpers do
  import Plug.Conn
  import Conduit.Fixture

  alias ConduitWeb.JWT

  def authenticated_conn(conn) do
    with {:ok, user} <- fixture(:user),
         {:ok, jwt} <- JWT.generate_jwt(user)
    do
      conn
      |> put_req_header("authorization", "Token " <> jwt)
    end
  end
end

Building the article controller

The integration test will initially fail because we have not yet configured a Phoenix route for the /api/articles path. We map this route to the article controller in lib/conduit_web/router.ex:

defmodule Conduit.Web.Router do
  use Conduit.Web, :router

  scope "/api", Conduit.Web do
    pipe_through :api

    post "/articles", ArticleController, :create
  end
end

Only authenticated users are allowed to publish articles. So we authenticate the request to the article controller using the two Guardian plugs Guardian.Plug.EnsureAuthenticated and Guardian.Plug.EnsureResource.

# lib/conduit_web/controllers/article_controller.ex
defmodule ConduitWeb.ArticleController do
  use ConduitWeb, :controller
  use Guardian.Phoenix.Controller

  alias Conduit.Blog
  alias Conduit.Blog.Projections.Article

  plug Guardian.Plug.EnsureAuthenticated, %{handler: ConduitWeb.ErrorHandler} when action in [:create]
  plug Guardian.Plug.EnsureResource, %{handler: ConduitWeb.ErrorHandler} when action in [:create]

  action_fallback ConduitWeb.FallbackController

  def create(conn, %{"article" => article_params}, user, _claims) do
    author = Blog.get_author!(user.uuid)

    with {:ok, %Article{} = article} <- Blog.publish_article(author, article_params) do
      conn
      |> put_status(:created)
      |> render("show.json", article: article)
    end
  end
end

The controller uses the Blog context to publish an article, using a new Blog.publish_article/2 function. It will follow our standard command dispatch pattern:

  1. Create a command from the user provided input parameters.
  2. Dispatch the command, thereby invoking its validation rules.
  3. Wait for the read model to be updated.
  4. Return the projected data from the read model.
defmodule Conduit.Blog do
  @doc """
  Publishes an article by the given author.
  """
  def publish_article(%Author{} = author, attrs \\ %{}) do
    uuid = UUID.uuid4()

    publish_article =
      attrs
      |> PublishArticle.new()
      |> PublishArticle.assign_uuid(uuid)
      |> PublishArticle.assign_author(author)
      |> PublishArticle.generate_url_slug()

    with :ok <- Router.dispatch(publish_article, consistency: :strong) do
      get(Article, uuid)
    else
      reply -> reply
    end
  end
end

You may notice that we assign the article’s author from the given user and must also generate a unique URL slug from the article title. These are important requirements for the feature, so we will write an integration test for the blog context and include tests to cover these.

# test/conduit/blog/blog_test.exs
defmodule Conduit.BlogTest do
  use Conduit.DataCase

  alias Conduit.Blog
  alias Conduit.Blog.Projections.Article

  describe "publish article" do
    setup [
      :create_author,
    ]

    @tag :integration
    test "should succeed with valid data", %{author: author} do
      assert {:ok, %Article{} = article} = Blog.publish_article(author, build(:article))

      assert article.slug == "how-to-train-your-dragon"
      assert article.title == "How to train your dragon"
      assert article.description == "Ever wonder how?"
      assert article.body == "You have to believe"
      assert article.tag_list == ["dragons", "training"]
      assert article.author_username == "jake"
      assert article.author_bio == nil
      assert article.author_image == nil
    end

    @tag :integration
    test "should generate unique URL slug", %{author: author} do
      assert {:ok, %Article{} = article1} = Blog.publish_article(author, build(:article))
      assert article1.slug == "how-to-train-your-dragon"

      assert {:ok, %Article{} = article2} = Blog.publish_article(author, build(:article))
      assert article2.slug == "how-to-train-your-dragon-2"
    end
  end

  defp create_author(_context) do
    {:ok, author} = fixture(:author)

    [author: author]
  end
end

The register_user/1 function is called before each test case. It provides a registered user to use within the tests, since a user is required to publish articles. The article parameters are built by reusing the factory function previously created for the article controller test.

Defining the publish article command

The publish article command, in lib/conduit/blog/commands/publish_article.ex, contains:

  1. A struct to hold the input data.
  2. Validation rules using the Vex library.
  3. Functions to assign the article unique identifier and its author.
  4. A function to generate a unique URL slug, using a separate Slugger module.
# lib/conduit/blog/commands/publish_article.ex
defmodule Conduit.Blog.Commands.PublishArticle do
  defstruct [
    article_uuid: "",
    author_uuid: "",
    slug: "",
    title: "",
    description: "",
    body: "",
    tag_list: [],
  ]

  use ExConstructor
  use Vex.Struct

  alias Conduit.Blog.Projections.Author
  alias Conduit.Blog.Slugger
  alias Conduit.Blog.Commands.PublishArticle

  validates :article_uuid, uuid: true

  validates :author_uuid, uuid: true

  validates :slug,
    presence: [message: "can't be empty"],
    format: [with: ~r/^[a-z0-9\-]+$/, allow_nil: true, allow_blank: true, message: "is invalid"],
    string: true,
    unique_article_slug: true

  validates :title, presence: [message: "can't be empty"], string: true

  validates :description, presence: [message: "can't be empty"], string: true

  validates :body, presence: [message: "can't be empty"], string: true

  validates :tag_list, by: &is_list/1

  @doc """
  Assign a unique identity
  """
  def assign_uuid(%PublishArticle{} = publish_article, uuid) do
    %PublishArticle{publish_article | article_uuid: uuid}
  end

  @doc """
  Assign the author
  """
  def assign_author(%PublishArticle{} = publish_article, %Author{uuid: uuid}) do
    %PublishArticle{publish_article | author_uuid: uuid}
  end

  @doc """
  Generate a unique URL slug from the article title
  """
  def generate_url_slug(%PublishArticle{title: title} = publish_article) do
    case Slugger.slugify(title) do
      {:ok, slug} -> %PublishArticle{publish_article | slug: slug}
      _ -> publish_article
    end
  end
end

defimpl Conduit.Support.Middleware.Uniqueness.UniqueFields, for: Conduit.Blog.Commands.PublishArticle do
  def unique(_command), do: [
    {:slug, "has already been taken"},
  ]
end

Generating a unique URL slug

A slug is part of the URL that identifies a page in human-readable keywords. As an example, given an article title of “Welcome to Conduit” the corresponding slug might be “welcome-to-conduit”.

We will use the Slugger package to generate a slug from an article title.

In mix.exs, add the slugger dependency:

defp deps do
  [
    # ...    
    {:slugger, "~> 0.2"},
  ]
end

Fetch and compile the mix dependencies:

$ mix do deps.get, deps.compile

One complication of URL slug generation is that each slug must be unique. A single slug can only be used one: two articles with the same title cannot share a slug.

We will wrap the slugger library with our own module, in lib/conduit/blog/slugger.ex. For each generated slug it will query the article read model to determine whether the slug has already been assigned. If it has, a suffix will be appended and retried. So “article” becomes “article-2”, “article-3”, “article-4”, and so on until an unclaimed slug is found.

# lib/conduit/blog/slugger.ex
defmodule Conduit.Blog.Slugger do
  alias Conduit.Blog

  @doc """
  Slugify the given text and ensure that it is unique.

  A slug will contain only alphanumeric characters (`a-z`, `0-9`) and the default separator character (`-`\
).

  If the generated slug is already taken, append a numeric suffix and keep incrementing until a unique slu\
g is found.

  ## Examples

    - "Example article" => "example-article", "example-article-2", "example-article-3", etc.
  """
  @spec slugify(String.t) :: {:ok, slug :: String.t} | {:error, reason :: term}
  def slugify(title) do
    title
    |> Slugger.slugify_downcase()
    |> ensure_unique_slug()
  end

  # Ensure the given slug is unique, if not increment the suffix and try again.
  defp ensure_unique_slug(slug, suffix \\ 1)
  defp ensure_unique_slug("", _suffix), do: ""
  defp ensure_unique_slug(slug, suffix) do
    suffixed_slug = suffixed(slug, suffix)

    case exists?(suffixed_slug) do
      true -> ensure_unique_slug(slug, suffix + 1)
      false -> {:ok, suffixed_slug}
    end
  end

  # Does the slug exist?
  defp exists?(slug) do
    case Blog.article_by_slug(slug) do
      nil -> false
      _ -> true
    end
  end

  defp suffixed(slug, 1), do: slug
  defp suffixed(slug, suffix), do: slug <> "-" <> to_string(suffix)
end

We need to provide a query to find an article by a slug:

# lib/conduit/blog/queries/article_by_slug.ex
defmodule Conduit.Blog.Queries.ArticleBySlug do
  import Ecto.Query

  alias Conduit.Blog.Projections.Article

  def new(slug) do
    from a in Article,
    where: a.slug == ^slug
  end
end

Made publicly available from the Blog context:

defmodule Conduit.Blog do
  @doc """
  Get an article by its URL slug, or return `nil` if not found.
  """
  def article_by_slug(slug) do
    slug
    |> String.downcase()
    |> ArticleBySlug.new()
    |> Repo.one()
  end
end

Finally, we add validation to the publish article command to ensure uniqueness:

# lib/conduit/blog/validators/unique_article_slug.ex
defmodule Conduit.Blog.Validators.UniqueArticleSlug do
  use Vex.Validator

  alias Conduit.Blog

  def validate(value, _options) do
    Vex.Validators.By.validate(value, [
      function: fn value -> !article_exists?(value) end,
      message: "has already been taken"
    ])
  end

  defp article_exists?(slug) do
    case Blog.article_by_slug(slug) do
      nil -> false
      _ -> true
    end
  end
end

Building the article aggregate

Publishing an article indicates that our domain model should comprise an article aggregate:

# lib/conduit/blog/aggregates/article.ex
defmodule Conduit.Blog.Aggregates.Article do
  defstruct [
    :uuid,
    :slug,
    :title,
    :description,
    :body,
    :tag_list,
    :author_uuid,
  ]

  alias Conduit.Blog.Aggregates.Article
  alias Conduit.Blog.Commands.PublishArticle
  alias Conduit.Blog.Events.ArticlePublished

  @doc """
  Publish an article
  """
  def execute(%Article{uuid: nil}, %PublishArticle{} = publish) do
    %ArticlePublished{
      article_uuid: publish.article_uuid,
      slug: publish.slug,
      title: publish.title,
      description: publish.description,
      body: publish.body,
      tag_list: publish.tag_list,
      author_uuid: publish.author_uuid,
    }
  end

  # state mutators

  def apply(%Article{} = article, %ArticlePublished{} = published) do
    %Article{article |
      uuid: published.article_uuid,
      slug: published.slug,
      title: published.title,
      description: published.description,
      body: published.body,
      tag_list: published.tag_list,
      author_uuid: published.author_uuid,
    }
  end
end

An aggregate may only reference other aggregates by their identity, not by reference, so we provide the author’s identity as part of the command (author_uuid). We don’t include the author’s username, or any other details, as the article does not need that information. It is only required in the read model. You will see an example of combining and denormalising data across aggregates in a read model projection when we build the article projector.

We create a unit test for the article aggregate to cover publishing an article:

# test/conduit/blog/aggregates/article_test.exs
defmodule Conduit.Blog.ArticleTest do
  use Conduit.AggregateCase, aggregate: Conduit.Blog.Aggregates.Article

  alias Conduit.Blog.Events.ArticlePublished

  describe "publish article" do
    @tag :unit
    test "should succeed when valid" do
      article_uuid = UUID.uuid4()
      author_uuid = UUID.uuid4()

      assert_events build(:publish_article, article_uuid: article_uuid, author_uuid: author_uuid), [
        %ArticlePublished{
          article_uuid: article_uuid,
          slug: "how-to-train-your-dragon",
          title: "How to train your dragon",
          description: "Ever wonder how?",
          body: "You have to believe",
          tag_list: ["dragons", "training"],
          author_uuid: author_uuid,
        }
      ]
    end
  end
end

The article published domain event defines a struct and uses the Poison JSON encoder:

# lib/conduit/blog/events/article_published.ex
defmodule Conduit.Blog.Events.ArticlePublished do
  @derive [Poison.Encoder]
  defstruct [
    :article_uuid,
    :author_uuid,
    :slug,
    :title,
    :description,
    :body,
    :tag_list,
  ]
end

Lastly, the publish article command is routed to the aggregate in lib/conduit/router.ex:

# lib/conduit/router.ex (diff)
defmodule Conduit.Router do
  alias Conduit.Blog.Aggregates.{Article,Author}
  alias Conduit.Blog.Commands.{CreateAuthor,PublishArticle}

  identify Article, by: :article_uuid, prefix: "article-"

  dispatch [PublishArticle], to: Article
end

Projecting the article read model

We’ve built the article domain model, handling writes, so let’s turn our attention to the read model projection.

In CQRS applications we aim to build read models that directly support the queries our application requires. The benefit of the separate read model is that we can have many views of our data, each perfectly suited to the query it was built for. We want performant reads, so we choose to denormalise data and minimise joins in the database.

Blog article read model

For the article read model we want to also include the author’s details. So the query becomes a simple SELECT from a single table, no joins needed. We define a database migration to create the blog_articles table, including the author:

# priv/repo/migrations/20170628134610_create_blog_article.exs
defmodule Conduit.Repo.Migrations.CreateConduit.Blog.Article do
  use Ecto.Migration

  def change do
    create table(:blog_articles, primary_key: false) do
      add :uuid, :uuid, primary_key: true
      add :slug, :text
      add :title, :text
      add :description, :text
      add :body, :text
      add :tag_list, {:array, :text}
      add :favorite_count, :integer
      add :published_at, :naive_datetime
      add :author_uuid, :uuid
      add :author_username, :text
      add :author_bio, :text
      add :author_image, :text

      timestamps()
    end

    create unique_index(:blog_articles, [:slug])
    create index(:blog_articles, [:author_uuid])
    create index(:blog_articles, [:author_username])
    create index(:blog_articles, [:published_at])
  end
end

Note we also take advantage of PostgreSQL and Ecto support for arrays for the tag_list column. We add indexes on the columns that will be used for querying to improve their performance.

The article read model defines the corresponding Ecto schema:

# lib/conduit/blog/projections/article.ex
defmodule Conduit.Blog.Projections.Article do
  use Ecto.Schema

  @primary_key {:uuid, :binary_id, autogenerate: false}

  schema "blog_articles" do
    field :slug, :string
    field :title, :string
    field :description, :string
    field :body, :string
    field :tag_list, {:array, :string}
    field :favorite_count, :integer, default: 0
    field :published_at, :naive_datetime
    field :author_uuid, :binary_id
    field :author_bio, :string
    field :author_image, :string
    field :author_username, :string

    timestamps()
  end
end
Blog author read model

We define a database migration to create the blog authors table:

# priv/repo/migrations/20170628162259_create_blog_author.exs
defmodule Conduit.Repo.Migrations.CreateConduit.Blog.Author do
  use Ecto.Migration

  def change do
    create table(:blog_authors, primary_key: false) do
      add :uuid, :uuid, primary_key: true
      add :user_uuid, :uuid
      add :username, :string
      add :bio, :string
      add :image, :string

      timestamps()
    end

    create unique_index(:blog_authors, [:user_uuid])
  end
end

A corresponding Ecto schema is built, containing the subset of the user details we’ll use for authors:

# lib/conduit/blog/projections/author.ex
defmodule Conduit.Blog.Projections.Author do
  use Ecto.Schema

  @primary_key {:uuid, :binary_id, autogenerate: false}

  schema "blog_authors" do
    field :user_uuid, :binary_id
    field :username, :string
    field :bio, :string
    field :image, :string

    timestamps()
  end
end
Projecting blog authors and articles

In the article projector we handle two domain events:

  1. UserRegistered to capture the author details.
  2. ArticlePublished to record each article.

We use Ecto.Multi.run/2 to lookup an author by their identity before creating the article read model.

# lib/conduit/blog/projectors/article.ex
defmodule Conduit.Blog.Projectors.Article do
  use Commanded.Projections.Ecto,
    name: "Blog.Projectors.Article",
    consistency: :strong

  alias Conduit.Blog.Events.{ArticlePublished,AuthorCreated}
  alias Conduit.Blog.Projections.{Article,Author}
  alias Conduit.Repo

  project %AuthorCreated{} = author do
    Ecto.Multi.insert(multi, :author, %Author{
      uuid: author.author_uuid,
      user_uuid: author.user_uuid,
      username: author.username,
      bio: nil,
      image: nil,
    })
  end

  project %ArticlePublished{} = published, %{created_at: published_at} do
    multi
    |> Ecto.Multi.run(:author, fn _changes -> get_author(published.author_uuid) end)
    |> Ecto.Multi.run(:article, fn %{author: author} ->
      article = %Article{
        uuid: published.article_uuid,
        slug: published.slug,
        title: published.title,
        description: published.description,
        body: published.body,
        tag_list: published.tag_list,
        favorite_count: 0,
        published_at: published_at,
        author_uuid: author.uuid,
        author_username: author.username,
        author_bio: author.bio,
        author_image: author.image,
      }

      Repo.insert(article)
    end)
  end

  defp get_author(uuid) do
    case Repo.get(Author, uuid) do
      nil -> {:error, :author_not_found}
      author -> {:ok, author}
    end
  end
end

A projector is guaranteed to handle events in the order they were published. Therefore we can be sure that, within the article projector, the author will have been created before they publish an article.

Publishing articles test

With the read model projection completed we can verify article publishing from the blog context by executing the tests.

$ mix test test/conduit/blog/blog_test.exs
Excluding tags: [:pending]
..
Finished in 2.1 seconds
2 tests, 0 failures

The final step is to confirm the article controller tests pass. Before doing so we must update the article view, responsible for formatting the data into the desired structure and returned as JSON data:

# lib/conduit_web/views/article_view.ex
defmodule ConduitWeb.ArticleView do
  use ConduitWeb, :view
  alias ConduitWeb.ArticleView

  def render("index.json", %{articles: articles}) do
    %{articles: render_many(articles, ArticleView, "article.json")}
  end

  def render("show.json", %{article: article}) do
    %{article: render_one(article, ArticleView, "article.json")}
  end

  def render("article.json", %{article: article}) do
    %{
      slug: article.slug,
      title: article.title,
      description: article.description,
      body: article.body,
      tagList: article.tag_list,
      createdAt: NaiveDateTime.to_iso8601(article.published_at),
      updatedAt: NaiveDateTime.to_iso8601(article.updated_at),
      favoritesCount: article.favorite_count,
      favorited: false,
      author: %{
        username: article.author_username,
        bio: article.author_bio,
        image: article.author_image,
        following: false,
      },
    }
  end
end

Then execute the article controller test:

$ mix test test/conduit_web/controllers/article_controller_test.exs
Excluding tags: [:pending]
.
Finished in 1.1 seconds
1 test, 0 failures

We’ve now successfully published an article, let’s move on to the queries we need to support.

Listing articles

Fetching and displaying articles is the principal feature of a blog. In Conduit we will support listing all articles and filtering by tag, favorited, and author. To support pagination, an offset and limit may be provided. By default, a GET /api/articles request returns the most recent articles globally.

The tag, author and favorited query parameter are used to filter results.

Filter by tag ?tag=AngularJS
Filter by author ?author=jake
Limit number of articles (default is 20) ?limit=20
Offset/skip number of articles (default is 0) ?offset=0

Example response body:

{
  "articles":[{
    "slug": "how-to-train-your-dragon",
    "title": "How to train your dragon",
    "description": "Ever wonder how?",
    "body": "It takes a Jacobian",
    "tagList": ["dragons", "training"],
    "createdAt": "2016-02-18T03:22:56.637Z",
    "updatedAt": "2016-02-18T03:48:35.824Z",
    "favorited": false,
    "favoritesCount": 0,
    "author": {
      "username": "jake",
      "bio": "I work at statefarm",
      "image": "https://i.stack.imgur.com/xHWG8.jpg",
      "following": false
    }
  }, {

    "slug": "how-to-train-your-dragon-2",
    "title": "How to train your dragon 2",
    "description": "So toothless",
    "body": "It a dragon",
    "tagList": ["dragons", "training"],
    "createdAt": "2016-02-18T03:22:56.637Z",
    "updatedAt": "2016-02-18T03:48:35.824Z",
    "favorited": false,
    "favoritesCount": 0,
    "author": {
      "username": "jake",
      "bio": "I work at statefarm",
      "image": "https://i.stack.imgur.com/xHWG8.jpg",
      "following": false
    }
  }],
  "articlesCount": 2
}

List articles controller test

Once again our starting point when building a feature is to define an integration test that verifies the behaviour according to the above API spec. In this case, a GET request should return all published articles, ordered by published date with the most recent articles first.

The setup function makes use of two helpers to seed appropriate test data: register_user/1 and publish_articles/1.

defmodule ConduitWeb.ArticleControllerTest do
  use ConduitWeb.ConnCase

  setup %{conn: conn} do
    {:ok, conn: put_req_header(conn, "accept", "application/json")}
  end

  describe "list articles" do
    setup [
      :create_author,
      :publish_articles,
    ]

    @tag :web
    test "should return published articles by date published", %{conn: conn} do
      conn = get conn, article_path(conn, :index)
      json = json_response(conn, 200)
      articles = json["articles"]
      first_created_at = Enum.at(articles, 0)["createdAt"]
      first_updated_at = Enum.at(articles, 0)["updatedAt"]
      second_created_at = Enum.at(articles, 1)["createdAt"]
      second_updated_at = Enum.at(articles, 1)["updatedAt"]

      assert json == %{
        "articles" => [
          %{
            "slug" => "how-to-train-your-dragon-2",
            "title" => "How to train your dragon 2",
            "description" => "So toothless",
            "body" => "It a dragon",
            "tagList" => ["dragons", "training"],
            "createdAt" => first_created_at,
            "updatedAt" => first_updated_at,
            "favorited" => false,
            "favoritesCount" => 0,
            "author" => %{
              "username" => "jake",
              "bio" => nil,
              "image" => nil,
              "following" => false,
            }
          },
          %{
            "slug" => "how-to-train-your-dragon",
            "title" => "How to train your dragon",
            "description" => "Ever wonder how?",
            "body" => "You have to believe",
            "tagList" => ["dragons", "training"],
            "createdAt" => second_created_at,
            "updatedAt" => second_updated_at,
            "favorited" => false,
            "favoritesCount" => 0,
            "author" => %{
              "username" => "jake",
              "bio" => nil,
              "image" => nil,
              "following" => false,
            }
          },
        ],
        "articlesCount" => 2,
      }
    end
  end

  defp create_author(_context) do
    {:ok, author} = fixture(:author, user_uuid: UUID.uuid4())

    [
      author: author,
    ]
  end

  defp publish_articles(%{author: author}) do
    fixture(:article, author: author)
    fixture(:article, author: author, title: "How to train your dragon 2", description: "So toothless", bo\
dy: "It a dragon")

    []
  end
end

In the article controller, we add an index/4 function to query the latest articles from the given request params, and render the articles as JSON. We include the total count of articles matching the request query in addition to the subset of paginated articles returned.

def index(conn, params, _user, _claims) do
  {articles, total_count} = Blog.list_articles(params)
  render(conn, "index.json", articles: articles, total_count: total_count)
end

This route is mapped inside the /api scope of the web router:

scope "/api", Conduit.Web do
  pipe_through :api

  get "/articles", ArticleController, :index
end

The total count is included in the article view:

# lib/conduit_web/views/article_view.ex
defmodule ConduitWeb.ArticleView do
  use ConduitWeb, :view
  alias ConduitWeb.ArticleView

  def render("index.json", %{articles: articles, total_count: total_count}) do
    %{
      articles: render_many(articles, ArticleView, "article.json"),
      articlesCount: total_count,
    }
  end

  def render("show.json", %{article: article}) do
    %{article: render_one(article, ArticleView, "article.json")}
  end

  def render("article.json", %{article: article}) do
    %{
      slug: article.slug,
      title: article.title,
      description: article.description,
      body: article.body,
      tagList: article.tag_list,
      createdAt: NaiveDateTime.to_iso8601(article.published_at),
      updatedAt: NaiveDateTime.to_iso8601(article.updated_at),
      favoritesCount: article.favorite_count,
      favorited: false,
      author: %{
        username: article.author_username,
        bio: article.author_bio,
        image: article.author_image,
        following: false,
      },
    }
  end
end

Querying latest articles

The article controller depends upon a new function in the blog context: Blog.list_articles/1

It delegates the actual fetching of articles from the database to a new ListArticles query module.

# lib/conduit/blog/blog.ex (diff)
defmodule Conduit.Blog do
  alias Conduit.Blog.Queries.{ArticleBySlug,ListArticles}

  @doc """
  Returns most recent articles globally by default.

  Provide tag, author or favorited query parameter to filter results.
  """
  @spec list_articles(params :: map()) :: {articles :: list(Article.t), article_count :: non_neg_integer()}
  def list_articles(params \\ %{}) do
    ListArticles.paginate(params, Repo)
  end
end

Unlike previous queries, we’ll provide the query with the repo module allowing it to execute a request to the database. This is because we need to execute two queries:

  1. Find the articles matching the query, and return a subset of the request page (using limit and offset).
  2. Count the total number of articles matching the query.
Paginated articles

The entries/2 function includes the pagination, limit and offset, and orders the articles by their published date with the most recent articles first.

Article count

The count/1 function selects only the article’s uuid field and executes a database aggregation to count the rows.

The map of parameters is parsed into an Options struct using the ExConstructor library. This provides us with type checking on the available keys, and default values when not present in the user provided params.

# lib/conduit/blog/queries/list_articles.ex
defmodule Conduit.Blog.Queries.ListArticles do
  import Ecto.Query

  alias Conduit.Blog.Projections.Article

  defmodule Options do
    defstruct [
      limit: 20,
      offset: 0,
    ]

    use ExConstructor
  end

  def paginate(params, repo) do
    options = Options.new(params)

    articles = query() |> entries(options) |> repo.all()
    total_count = query() |> count() |> repo.aggregate(:count, :uuid)

    {articles, total_count}
  end

  defp query do
    from(a in Article)
  end

  defp entries(query, %Options{limit: limit, offset: offset}) do
    query
    |> order_by([a], desc: a.published_at)
    |> limit(^limit)
    |> offset(^offset)
  end

  defp count(query) do
    query |> select([:uuid])
  end
end

We’ll extend the blog test to include listing articles, ensuring that pagination is working as expected. Included in the test is a convenience function to publish multiple articles: publish_articles/1

defmodule Conduit.BlogTest do
  use Conduit.DataCase

  describe "list articles" do
    setup [
      :create_author,
      :publish_articles,
    ]

    @tag :integration
    test "should list articles by published date", %{articles: [article1, article2]} do
      assert {[article2, article1], 2} == Blog.list_articles()
    end

    @tag :integration
    test "should limit articles", %{articles: [_article1, article2]} do
      assert {[article2], 2} == Blog.list_articles(%{limit: 1})
    end

    @tag :integration
    test "should paginate articles", %{articles: [article1, _article2]} do
      assert {[article1], 2} == Blog.list_articles(%{offset: 1})
    end
  end

  defp publish_articles(%{user: user}) do
    {:ok, article1} = fixture(:article, author: user)
    {:ok, article2} = fixture(:article, author: user, title: "How to train your dragon 2", description: "S\
o toothless", body: "It a dragon")

    [
      articles: [article1, article2],
    ]
  end
end

Filter by author

Let’s start with filtering articles by their author. The test cases we’ll add cover the two scenarios where an author has, or has not, published any articles:

@tag :integration
test "should filter by author" do
  assert {[], 0} == Blog.list_articles(%{author: "unknown"})
end

@tag :integration
test "should filter by author returning only their articles", %{articles: [article1, article2]} do
  assert {[article2, article1], 2} == Blog.list_articles(%{author: "jake"})
end

For the test to pass we make a small change to the existing Ecto query, in Conduit.Blog.Queries.ListArticles, to add a WHERE clause that matches the author_username field with a given author value. The query is returned unmodified when the author is nil.

defp query(options) do
  from(a in Article)
  |> filter_by_author(options)
end

defp filter_by_author(query, %Options{author: nil}), do: query
defp filter_by_author(query, %Options{author: author}) do
  query |> where(author_username: ^author)
end

Populating the author field in the Options struct from the map of params from the request is handled by our use of ExConstructor (Options.new(params)).

Filter by tag

We’ve defined the tag_list field in the articles table as an array of text. We can use PostgreSQL’s built in support for searching inside arrays.

The SQL statement to query for a tag within an article’s tags array uses the ANY keyword:

SELECT * FROM blog_articles WHERE 'dragons' = ANY (tag_list);

This SQL is translated to the following Ecto query in our ListArticles module:

defp filter_by_tag(query, %Options{tag: nil}), do: query
defp filter_by_tag(query, %Options{tag: tag}) do
  from a in query,
  where: ^tag in a.tag_list
end

Unfortunately this is not a performant query to execute as it will require a sequential table scan.

We can analyse the query plan for our tag query as follows:

SET enable_seqscan = off;
EXPLAIN SELECT * FROM blog_articles WHERE 'dragons' = ANY (tag_list);

The result shows the query planner has chosen to execute a sequential scan on the blog_articles table:

Seq Scan on blog_articles  (cost=10000000000.00..10000000001.07 rows=1 width=316)
  Filter: ('dragons'::text = ANY (tag_list))

That’s bad news: our query will gradually degrade in performance over time as our blogging platform gains in popularity, encouraging more authors to publish their own articles. However, we can remedy this situation before it causes a problem in production by optimising the query.

Tagged articles table

One of the advantages of CQRS is that our read model can be built for the exact queries it must support. We could create a separate table for tagged articles and insert one entry for each tag assigned to a published article.

article_uuid tag author_username published_at
18e760d2-04f1-4da6-b27a-fca6d3ef1fa0 dragons jake 2017-07-28 12:00:00.000000
18e760d2-04f1-4da6-b27a-fca6d3ef1fa0 training jake 2017-07-28 12:00:00.000000
62acb90d-5ea3-4c0a-9145-2d397fc5750f cqrs ben 2017-07-30 14:00:00.000000

Using an index on the tag column would allow performant lookup.

Use PostgreSQL’s GIN index

We can take advantage of PostgreSQL’s GIN indexes, thereby we don’t need to build a separate table.

GIN indexes are inverted indexes which can handle values that contain more than one key, arrays for example.

To add a GIN index we create an Ecto database migration and specify the type of index via the :using option:

# priv/repo/migrations/20170719085120_add_index_to_blog_tags.exs
defmodule Conduit.Repo.Migrations.AddIndexToBlogTags do
  use Ecto.Migration

  def change do
    create index(:blog_articles, [:tag_list], using: "GIN")
  end
end

Then run the migration:

mix ecto.migrate

After adding the GIN index on the tag_list column we can execute the following query:

SELECT * FROM blog_articles WHERE tag_list @> '{dragons}';

The @> clause is used to match rows where the array contains the given value. It’s the same behaviour as the ANY query we initially wrote, but performs significantly better as it can use the index.

set enable_seqscan = off;
EXPLAIN SELECT * FROM blog_articles WHERE tag_list @> '{dragons}';

Now the query planner is taking advantage of our GIN index:

Bitmap Heap Scan on blog_articles  (cost=2.01..3.02 rows=1 width=316)
  Recheck Cond: (tag_list @> '{dragons}'::text[])
  ->  Bitmap Index Scan on blog_articles_tag_list_index  (cost=0.00..2.01 rows=1 width=0)
        Index Cond: (tag_list @> '{dragons}'::text[])

We now need to update the filter by tag query in the ListArticles module. We use Ecto’s fragment function to provide the exact SQL syntax required for the GIN array clause:

defp filter_by_tag(query, %Options{tag: nil}), do: query
defp filter_by_tag(query, %Options{tag: tag}) do
  from a in query,
  where: fragment("? @> ?", a.tag_list, [^tag])
end

Finally, we verify this query succeeds by adding two new integration tests to the blog test:

@tag :integration
test "should filter by tag returning only tagged articles", %{articles: [article1, _article2]} do
  assert {[article1], 1} == Blog.list_articles(%{tag: "believe"})
end

@tag :integration
test "should filter by tag" do
  assert {[], 0} == Blog.list_articles(%{tag: "unknown"})
end

Get an article

Now we can list and filter articles, the next feature is to get a single article by its unique URL slug. First up is an integration test that creates an author, publishes an article, and attempts to get the newly published article:

defmodule ConduitWeb.ArticleControllerTest do
  describe "get article" do
    setup [
      :create_author,
      :publish_article,
    ]

    @tag :web
    test "should return published article by slug", %{conn: conn} do
      conn = get conn, article_path(conn, :show, "how-to-train-your-dragon")
      json = json_response(conn, 200)
      article = json["article"]
      created_at = article["createdAt"]
      updated_at = article["updatedAt"]

      assert json == %{
        "article" => %{
          "slug" => "how-to-train-your-dragon",
          "title" => "How to train your dragon",
          "description" => "Ever wonder how?",
          "body" => "You have to believe",
          "tagList" => ["dragons", "training"],
          "createdAt" => created_at,
          "updatedAt" => updated_at,
          "favorited" => false,
          "favoritesCount" => 0,
          "author" => %{
            "username" => "jake",
            "bio" => nil,
            "image" => nil,
            "following" => false,
          }
        },
      }
    end
  end

  defp publish_article(%{author: author}) do
    {:ok, article} = fixture(:article, author: author)

    [
      article: article,
    ]
  end
end

The test fails, so let’s go and make the changes necessary for it to pass. We need to route the article slug URL in the Phoenix web router module:

defmodule ConduitWeb.Router do
  scope "/api", ConduitWeb do
    pipe_through :api

    get "/articles/:slug", ArticleController, :show
  end
end

This route is mapped to a new show/4 function in the ArticleController which retrieves the article by its unique slug and renders it as JSON:

# lib/conduit_web/controllers/article_controller.ex (diff)
defmodule ConduitWeb.ArticleController do
  def show(conn, %{"slug" => slug}, _user, _claims) do
    article = Blog.article_by_slug!(slug)
    render(conn, "show.json", article: article)
  end
end

Since we want to return a 404 HTTP error response when the article does not exist we add a Blog.article_by_slug!/1 function, note the ! suffix, which raises an error when the query returns nothing. This will be handled by the FallbackController to render the appropriate HTTP status.

defmodule Conduit.Blog do
  @doc """
  Get an article by its URL slug, or raise an `Ecto.NoResultsError` if not found
  """
  def article_by_slug!(slug),
    do: article_by_slug_query(slug) |> Repo.one!()

  defp article_by_slug_query(slug) do
    slug
    |> String.downcase()
    |> ArticleBySlug.new()
  end
end

That’s all we need to get an article from our API since most of the heavy lifting was already implemented by us earlier to support listing multiple articles.

Favorite articles

The API spec to favorite and unfavorite an article is as follows:

HTTP verb URL Required fields
POST /api/articles/:slug/favorite None
DELETE /api/articles/:slug/favorite None

There’s no request body or required fields since the URL contains the article being favorited, the HTTP verb informs us of the operation, and the authenticated user making the request is the person who’s favorite it is.

To implement this feature we’ll need to add two new commands, and associated domain events, one to favorite an article and another to unfavorite. When deciding which aggregate should be responsible for this behaviour we need to consider the invariants to be protected. In this example we’d like to ensure a user may only favorite an article once, and they may only unfavorite an article they have previously favourited. We can use the article aggregate to enforce these rules by having each article track who has favorited it.

Favorite integration test

There’s no surprises that we start building the favoriting feature by writing an integration test to specify the required behaviour. In the setup function defined in the favorite article controller test we seed an initial author, publish an article, and register a user. This user will be used to make the authenticated requests to favorite the published article.

# test/conduit_web/controllers/favorite_article_controller_test.exs
defmodule ConduitWeb.FavoriteArticleControllerTest do
  use ConduitWeb.ConnCase

  setup %{conn: conn} do
    {:ok, conn: put_req_header(conn, "accept", "application/json")}
  end

  describe "favorite article" do
    setup [
      :create_author,
      :publish_article,
      :register_user,
    ]

    @tag :web
    test "should be favorited and return article", %{conn: conn, user: user} do
      conn = post authenticated_conn(conn, user), favorite_article_path(conn, :create, "how-to-train-your-\
dragon")
      json = json_response(conn, 201)["article"]
      created_at = json["createdAt"]
      updated_at = json["updatedAt"]

      assert json == %{
        "slug" => "how-to-train-your-dragon",
        "title" => "How to train your dragon",
        "description" => "Ever wonder how?",
        "body" => "You have to believe",
        "tagList" => ["dragons", "training"],
        "createdAt" => created_at,
        "updatedAt" => updated_at,
        "favorited" => true,
        "favoritesCount" => 1,
        "author" => %{
          "username" => "jake",
          "bio" => nil,
          "image" => nil,
          "following" => false,
        }
      }
    end
  end

  describe "unfavorite article" do
    setup [
      :create_author,
      :publish_article,
      :register_user,
      :get_author,
      :favorite_article,
    ]

    @tag :web
    test "should be unfavorited and return article", %{conn: conn, user: user} do
      conn = delete authenticated_conn(conn, user), favorite_article_path(conn, :delete, "how-to-train-you\
r-dragon")
      json = json_response(conn, 201)["article"]
      created_at = json["createdAt"]
      updated_at = json["updatedAt"]

      assert json == %{
        "slug" => "how-to-train-your-dragon",
        "title" => "How to train your dragon",
        "description" => "Ever wonder how?",
        "body" => "You have to believe",
        "tagList" => ["dragons", "training"],
        "createdAt" => created_at,
        "updatedAt" => updated_at,
        "favorited" => false,
        "favoritesCount" => 0,
        "author" => %{
          "username" => "jake",
          "bio" => nil,
          "image" => nil,
          "following" => false,
        }
      }
    end
  end
end

You’ll notice that we have an assertion to check the favorited flag is correctly toggled and the favoritesCount is incremented and decremented when a user favorites or unfavorites an article. Running this test will immediately fail as we haven’t defined the favourite_article path in our Phoenix router nor built the FavoriteArticleController.

Article routing

We need to route POST and DELETE requests to the /api/articles/:slug/favorite URL. Both of these actions will require fetching the article by its slug. We could do this query in the controller, but Phoenix’s router allows us to define our own request handling pipeline and include any custom plug modules or functions. We’ll take advantage of this feature to define an :article pipeline with a plug module which attempts to load an article by the slug contained within the URL. This pipeline will be used for any requests matching the /api/articles/:slug path, including the new favorite article controller.

defmodule ConduitWeb.Router do
  alias ConduitWeb.Plugs

  pipeline :article do
    plug Plugs.LoadArticleBySlug
  end

  scope "/api", ConduitWeb do
    scope "/articles/:slug" do
      pipe_through :article

      post "/favorite", FavoriteArticleController, :create
      delete "/favorite", FavoriteArticleController, :delete
    end
  end
end

The LoadArticleBySlug plug extracts the slug from the request params and fetches the article from the database. The article is assigned to the connection, allowing it to be accessed later in a controllet action using %{assigns: %{article: article}}.

# lib/conduit_web/plugs/load_article_by_slug.ex
defmodule ConduitWeb.Plugs.LoadArticleBySlug do
  use Phoenix.Controller, namespace: ConduitWeb

  import Plug.Conn

  alias Conduit.Blog

  def init(opts), do: opts

  def call(%Plug.Conn{params: %{"slug" => slug}} = conn, _opts) do
    article = Blog.article_by_slug!(slug)

    assign(conn, :article, article)
  end
end

Favorite article controller

Let’s go ahead and build the favorite article controller. You’ll notice that we require an authenticated user for the create and delete controller actions. This allows us to lookup the author associated with the current user. It’s the author who will favorite, or unfavorite, an article. For both controller actions we return a JSON represenation of the article. As previously mentioned, the article has already been retrieved from the database and made available within the assigns map by the LoadArticleBySlug plug.

# lib/conduit_web/controllers/favorite_article_controller.ex
defmodule ConduitWeb.FavoriteArticleController do
  use ConduitWeb, :controller
  use Guardian.Phoenix.Controller

  alias Conduit.Blog
  alias Conduit.Blog.Projections.Article
  alias ConduitWeb.ArticleView

  plug Guardian.Plug.EnsureAuthenticated, %{handler: ConduitWeb.ErrorHandler} when action in [:create, :de\
lete]
  plug Guardian.Plug.EnsureResource, %{handler: ConduitWeb.ErrorHandler} when action in [:create, :delete]

  action_fallback ConduitWeb.FallbackController

  def create(%{assigns: %{article: article}} = conn, _params, user, _claims) do
    author = Blog.get_author!(user.uuid)

    with {:ok, %Article{} = article} <- Blog.favorite_article(article, author) do
      conn
      |> put_status(:created)
      |> render(ArticleView, "show.json", article: article)
    end
  end

  def delete(%{assigns: %{article: article}} = conn, _params, user, _claims) do
    author = Blog.get_author!(user.uuid)

    with {:ok, %Article{} = article} <- Blog.unfavorite_article(article, author) do
      conn
      |> put_status(:created)
      |> render(ArticleView, "show.json", article: article)
    end
  end
end

Favorite articles in Blog context

The FavoriteArticleController expects Blog.favorite_article/2 and Blog.unfavorite_article/2 functions to exist in the blog context, so we’ll add them to the public API exposed by Conduit.Blog. Both functions construct a command, dispatch it using the router, and then query the read model to return updated data. This helps us to identify the two new command we need to define: FavoriteArticle and UnfavoriteArticle.

defmodule Conduit.Blog do
  @doc """
  Favorite the article for an author
  """
  def favorite_article(%Article{uuid: article_uuid}, %Author{uuid: author_uuid}) do
    favorite_article = %FavoriteArticle{
      article_uuid: article_uuid,
      favorited_by_author_uuid: author_uuid,
    }

    with :ok <- Router.dispatch(favorite_article, consistency: :strong),
         {:ok, article} <- get(Article, article_uuid) do
      {:ok, %Article{article | favorited: true}}
    else
      reply -> reply
    end
  end

  @doc """
  Unfavorite the article for an author
  """
  def unfavorite_article(%Article{uuid: article_uuid}, %Author{uuid: author_uuid}) do
    unfavorite_article = %UnfavoriteArticle{
      article_uuid: article_uuid,
      unfavorited_by_author_uuid: author_uuid,
    }

    with :ok <- Router.dispatch(unfavorite_article, consistency: :strong),
         {:ok, article} <- get(Article, article_uuid) do
      {:ok, %Article{article | favorited: false}}
    else
      reply -> reply
    end
  end
end

You might have noticed we manually set the favorited flag on the Article schema and are wondering how does that work? The answer is that a virtual field has been added to the Ecto schema which allows us to set the value but it’s not backed by a column in the database. This is useful for fields used for setting transient values related to a single query. In this case it’s whether the author has favorited the article. We can manually set the value in these functions because we know what the actual value should be without requiring any further database interaction, such as querying for an author’s favorites. It’s a small performance optimisation.

# lib/conduit/blog/projections/article.ex (diff)
defmodule Conduit.Blog.Projections.Article do
    field :favorited, :boolean, virtual: true, default: false
end

Favorite commands and events

The favorite article command requires only the article and author identifiers, which are validated as UUIDs:

# lib/conduit/blog/commands/favorite_article.ex
defmodule Conduit.Blog.Commands.FavoriteArticle do
  defstruct [
    article_uuid: "",
    favorited_by_author_uuid: "",
  ]

  use ExConstructor
  use Vex.Struct

  validates :article_uuid, uuid: true
  validates :favorited_by_author_uuid, uuid: true
end

The article favorited domain event includes both identifiers, but also contains a favorite_count field. This is included by the article aggregate as a count of favorites so that it can be later projected into the read model.

# lib/conduit/blog/events/article_favorited.ex
defmodule Conduit.Blog.Events.ArticleFavorited do
  @derive [Poison.Encoder]
  defstruct [
    :article_uuid,
    :favorited_by_author_uuid,
    :favorite_count,
  ]
end

The unfavorite command and events follow the same pattern, so have been omitted.

Favorite article aggregate handling

As discussed earlier, the article must now track which authors and how many in total have favorited it. We add favorited_by_authors and favorite_count fields to the aggregate’s state.

When favoriting an article we must ensure the author hasn’t already favorited the article. This rule is checked by the is_favorited?/2 helper function which looks to see if the author is already in the favorited_by_authors MapSet. If the author’s identity is already present, the favorite article command returns nil. This allows the command to be idempotent; if an author requests to favorite an article they’ve already favorited then we can just ignore their request. The unfavorite command is handled similarly, except the check is to ensure the article has already been favorited by the author before unfavoriting.

We also ensure an author can only (un)favorite an existing article by pattern matching on the article’s identity field, using %Article{uuid: nil} in the execute/2 function. An error is returned when it’s nil, indicating an article that has never been published.

The favorite_count value is calculated and included in the events in the (un)favorite command handling functions. This allows the apply/2 state mutator functions to be logic free, they only need to copy the value from the event to the aggregate’s state. We want to ensure there’s minimal processing being done in the apply/2 functions.

defmodule Conduit.Blog.Aggregates.Article do
  defstruct [
    # ...
    favorited_by_authors: MapSet.new(),
    favorite_count: 0
  ]

  @doc """
  Favorite the article for an author
  """
  def execute(%Article{uuid: nil}, %FavoriteArticle{}), do: {:error, :article_not_found}
  def execute(
    %Article{uuid: uuid, favorite_count: favorite_count} = article,
    %FavoriteArticle{favorited_by_author_uuid: author_id})
  do
    case is_favorited?(article, author_id) do
      true -> nil
      false ->
        %ArticleFavorited{
          article_uuid: uuid,
          favorited_by_author_uuid: author_id,
          favorite_count: favorite_count + 1,
        }
    end
  end

  @doc """
  Unfavorite the article for the user
  """
  def execute(%Article{uuid: nil}, %UnfavoriteArticle{}), do: {:error, :article_not_found}
  def execute(
    %Article{uuid: uuid, favorite_count: favorite_count} = article,
    %UnfavoriteArticle{unfavorited_by_author_uuid: author_id})
  do
    case is_favorited?(article, author_id) do
      true ->
        %ArticleUnfavorited{
          article_uuid: uuid,
          unfavorited_by_author_uuid: author_id,
          favorite_count: favorite_count - 1,
        }
      false -> nil
    end
  end

  def apply(
    %Article{favorited_by_authors: favorited_by} = article,
    %ArticleFavorited{favorited_by_author_uuid: author_id, favorite_count: favorite_count})
  do
    %Article{article |
      favorited_by_authors: MapSet.put(favorited_by, author_id),
      favorite_count: favorite_count,
    }
  end

  def apply(
    %Article{favorited_by_authors: favorited_by} = article,
    %ArticleUnfavorited{unfavorited_by_author_uuid: author_id, favorite_count: favorite_count})
  do
    %Article{article |
      favorited_by_authors: MapSet.delete(favorited_by, author_id),
      favorite_count: favorite_count,
    }
  end

  # Is the article a favorite of the user?
  defp is_favorited?(%Article{favorited_by_authors: favorited_by}, user_uuid) do
    MapSet.member?(favorited_by, user_uuid)
  end
end

Unit testing favorites in the article aggregate

We can test the newly added behaviour to the article aggregate by extending the article unit test and reusing the assert_events/3 test helper function provided by the Conduit.AggregateCase ExUnit case template module. The assert events function takes a list of initial events, used to populate the aggregate’s state, a command to execute, and a list of expected events to be produced by the aggregate. To favorite an article we need to seed the aggregate with an article published event, produced by the factory function defined in Conduit.Factory.

# test/conduit/blog/aggregates/article_test.exs
defmodule Conduit.Blog.ArticleTest do
  use Conduit.AggregateCase, aggregate: Conduit.Blog.Aggregates.Article

  alias Conduit.Blog.Commands.{
    FavoriteArticle,
    UnfavoriteArticle
  }

  alias Conduit.Blog.Events.{
    ArticleFavorited,
    ArticleUnfavorited
  }

  describe "publish article" do
    @tag :unit
    test "should succeed when valid" do
      article_uuid = UUID.uuid4()
      author_uuid = UUID.uuid4()

      assert_events(
        build(:publish_article, article_uuid: article_uuid, author_uuid: author_uuid),
        [
          build(:article_published, article_uuid: article_uuid, author_uuid: author_uuid)
        ]
      )
    end
  end

  describe "favorite article" do
    @tag :unit
    test "should succeed when not already a favorite" do
      article_uuid = UUID.uuid4()
      author_uuid = UUID.uuid4()

      assert_events(
        build(:article_published, article_uuid: article_uuid, author_uuid: author_uuid),
        %FavoriteArticle{article_uuid: article_uuid, favorited_by_author_uuid: author_uuid},
        [
          %ArticleFavorited{
            article_uuid: article_uuid,
            favorited_by_author_uuid: author_uuid,
            favorite_count: 1
          }
        ]
      )
    end

    @tag :unit
    test "should ignore when already a favorite" do
      article_uuid = UUID.uuid4()
      author_uuid = UUID.uuid4()

      assert_events(
        build(:article_published, article_uuid: article_uuid, author_uuid: author_uuid),
        [
          %FavoriteArticle{article_uuid: article_uuid, favorited_by_author_uuid: author_uuid},
          %FavoriteArticle{article_uuid: article_uuid, favorited_by_author_uuid: author_uuid}
        ],
        []
      )
    end
  end

  describe "unfavorite article" do
    @tag :unit
    test "should succeed when a favorite" do
      article_uuid = UUID.uuid4()
      author_uuid = UUID.uuid4()

      assert_events(
        build(:article_published, article_uuid: article_uuid, author_uuid: author_uuid),
        [
          %FavoriteArticle{article_uuid: article_uuid, favorited_by_author_uuid: author_uuid},
          %UnfavoriteArticle{article_uuid: article_uuid, unfavorited_by_author_uuid: author_uuid}
        ],
        [
          %ArticleUnfavorited{
            article_uuid: article_uuid,
            unfavorited_by_author_uuid: author_uuid,
            favorite_count: 0
          }
        ]
      )
    end

    @tag :unit
    test "should ignore when not a favorite" do
      article_uuid = UUID.uuid4()
      author_uuid = UUID.uuid4()

      assert_events(
        build(:article_published, article_uuid: article_uuid, author_uuid: author_uuid),
        [
          %UnfavoriteArticle{article_uuid: article_uuid, unfavorited_by_author_uuid: author_uuid}
        ],
        []
      )
    end
  end
end

Routing favorite commands

With the article aggregate modified to handle the new favorite commands, we need to finish off by routing the commands in the Conduit.Router module:

# lib/conduit/router.ex (diff)
defmodule Conduit.Router do
  alias Conduit.Blog.Commands.{CreateAuthor,FavoriteArticle,PublishArticle,UnfavoriteArticle}

  dispatch [
    PublishArticle,
    FavoriteArticle,
    UnfavoriteArticle
  ], to: Article
end

Projecting favorite articles in the read model

We’re going to use a SQL join table to track favorited articles which we’ll later use to filter articles favorited by a user.

# lib/conduit/blog/favorited_article.ex
defmodule Conduit.Blog.Projections.FavoritedArticle do
  use Ecto.Schema

  @primary_key false

  schema "blog_favorited_articles" do
    field :article_uuid, :binary_id, primary_key: true
    field :favorited_by_author_uuid, :binary_id, primary_key: true

    timestamps()
  end
end

After creating and running a database migration to add the new blog_favorited_articles join table we can extend the article projector to handle the new favorite and unfavorite events. Whenever an article is favorited we insert a row into the join table, on unfavorite the row is deleted.

Aditionaly, we also update the article’s favorite_count field from the count included in the events. Remember that we try to keep our projection code as simple as possible, preferring to keep calculation logic in the domain model (our aggregates). We chain together the two Ecto.Multi operations which update the join table and articles table using Elixir’s pipeline syntax (|>).

# lib/conduit/blog/projectors/article.ex (diff)
defmodule Conduit.Blog.Projectors.Article do
  alias Conduit.Blog.Projections.{Article,Author,FavoritedArticle}
  alias Conduit.Blog.Events.{
    ArticleFavorited,
    ArticlePublished,
    ArticleUnfavorited,
    AuthorCreated,
  }

  @doc """
  Update favorite count when an article is favorited
  """
  project %ArticleFavorited{article_uuid: article_uuid, favorited_by_author_uuid: favorited_by_author_uuid\
, favorite_count: favorite_count} do
    multi
    |> Ecto.Multi.insert(:favorited_article, %FavoritedArticle{article_uuid: article_uuid, favorited_by_au\
thor_uuid: favorited_by_author_uuid})
    |> Ecto.Multi.update_all(:article, article_query(article_uuid), set: [
      favorite_count: favorite_count,
    ])
  end

  @doc """
  Update favorite count when an article is unfavorited
  """
  project %ArticleUnfavorited{article_uuid: article_uuid, unfavorited_by_author_uuid: unfavorited_by_autho\
r_uuid, favorite_count: favorite_count} do
    multi
    |> Ecto.Multi.delete_all(:favorited_article, favorited_article_query(article_uuid, unfavorited_by_auth\
or_uuid))
    |> Ecto.Multi.update_all(:article, article_query(article_uuid), set: [
      favorite_count: favorite_count,
    ])
  end

  defp article_query(article_uuid) do
    from(a in Article, where: a.uuid == ^article_uuid)
  end

  defp favorited_article_query(article_uuid, author_uuid) do
    from(f in FavoritedArticle, where: f.article_uuid == ^article_uuid and f.favorited_by_author_uuid == ^\
author_uuid)
  end
end

Favorite articles test

With the read model projection complete we can now run our favorite article tests and should see them pass, which they do. However we’re not quite finished yet. Remmeber when we favorite, or unfavorite, an article we manually set the favorited field depending upon what the expected outcome would be. This works fine for this use case, but if we later view all articles the field isn’t being set and defaults to false.

We can demonstrate this problem with the following integration test which favorites a published article and then immediately requests all articles.

describe "list articles including favorited" do
  setup [
    :create_author,
    :publish_articles,
    :register_user,
    :get_author,
    :favorite_article,
  ]

  @tag :web
  test "should return published articles by date published", %{conn: conn, user: user} do
    conn = get authenticated_conn(conn, user), article_path(conn, :index)
    json = json_response(conn, 200)
    articles = json["articles"]
    first_created_at = Enum.at(articles, 0)["createdAt"]
    first_updated_at = Enum.at(articles, 0)["updatedAt"]
    second_created_at = Enum.at(articles, 1)["createdAt"]
    second_updated_at = Enum.at(articles, 1)["updatedAt"]

    assert json == %{
      "articles" => [
        %{
          "slug" => "how-to-train-your-dragon-2",
          "title" => "How to train your dragon 2",
          "description" => "So toothless",
          "body" => "It a dragon",
          "tagList" => ["dragons", "training"],
          "createdAt" => first_created_at,
          "updatedAt" => first_updated_at,
          "favorited" => false,
          "favoritesCount" => 0,
          "author" => %{
            "username" => "jake",
            "bio" => nil,
            "image" => nil,
            "following" => false,
          }
        },
        %{
          "slug" => "how-to-train-your-dragon",
          "title" => "How to train your dragon",
          "description" => "Ever wonder how?",
          "body" => "You have to believe",
          "tagList" => ["dragons", "training", "believe"],
          "createdAt" => second_created_at,
          "updatedAt" => second_updated_at,
          "favorited" => true,
          "favoritesCount" => 1,
          "author" => %{
            "username" => "jake",
            "bio" => nil,
            "image" => nil,
            "following" => false,
          }
        },
      ],
      "articlesCount" => 2,
    }
  end
end

Our expectation is the favorited flag will be true for the article we just favorited, but running the test shows that it’s false.

1) test list articles including favorited should return published articles by date published (ConduitWeb.A\
rticleControllerTest)
     test/conduit_web/controllers/article_controller_test.exs:106

Determining whether an article is favorited or not requires us to look in the blog_favorited_articles join table. When a row exists for the article and author it’s a favorite, otherwise it’s not. To implement this conditional checking we modify the Conduit.Blog.Queries.ListArticles module and extend the query. A left join is used to compare whether a field from the joined table is not nil (not is_nil indicating no row exists) to set the virtual favorited flag.

defp include_favorited_by_author(query, nil), do: query
defp include_favorited_by_author(query, %Author{uuid: author_uuid}) do
  from a in query,
  left_join: f in FavoritedArticle, on: [article_uuid: a.uuid, favorited_by_author_uuid: ^author_uuid],
  select: %{a | favorited: not is_nil(f.article_uuid)}
end

Now we’re able to run the full test suite and see it passing.