Back to Papercups

Add Conversations Api

docs/examples/add_conversations_api.md

1.0.05.3 KB
Original Source

These notes outline how I set up a new API endpoint for the conversations resource.

Generate Conversations context

This command generates a Conversation model/schema within a Conversations context, with some initial fields for the conversations table in the DB:

mix phx.gen.context Conversations Conversation conversations status:string

The files created by this command are:

lib/chat_api/conversations/conversation.ex
priv/repo/migrations/20200710123644_create_conversations.exs
lib/chat_api/conversations.ex
test/chat_api/conversations_test.exs
  • lib/chat_api/conversations/conversation.ex defines the schema for the Conversation model
  • priv/repo/migrations/2020xxxx_create_conversations.exs sets up the migration script for the conversations table in the DB
  • lib/chat_api/conversations.ex sets up the boilerplate CRUD methods (e.g. Conversations.list_conversations())
  • test/chat_api/conversations_test.exs autogenerates some tests for these CRUD methods

Adding the association between conversations and messages

After these are generated, in order to set up the association between conversations and messages, we run another migration script to add a conversation_id to the messages table:

mix ecto.gen.migration add_conversation_id_to_messages

...which simply creates a new migration script (e.g. priv/repo/migrations/2020xxxx_add_conversation_id_to_messages.exs), where we add the foreign key to the messages table:

ex
# ...
def change do
  alter table(:messages) do
    add(:conversation_id, references(:conversations, type: :uuid, on_delete: :delete_all))
  end

  create(index(:messages, [:conversation_id]))
end

After that, we can add the has_many and belongs_to relationships to the Conversation and Message schema:

ex
# in conversation.ex
alias ChatApi.Messages.Message

schema "conversations" do
  # ...
  has_many(:messages, Message)

  # ...
end

# and in message.ex
alias ChatApi.Conversations.Conversation

schema "messages" do
  # ...
  belongs_to(:conversation, Conversation)

  # ...
end

Included associated resources in queries

Now if we want to load the associated messages in a query for a conversation, all we have to do is call Repo.preload(:messages) like so:

ex
def get_conversation!(id) do
  Conversation |> Repo.get!(id) |> Repo.preload(:messages)
end

Generating the controllers and views

Run this command to generate the controller and JSON views for the conversations API:

mix phx.gen.json Conversations Conversation conversations status:string --no-context --no-schema

(Note that this is really similar to our initial command to set up the context -- it might actually be possible to just run this upfront, but I need to double check. For now, since we already have the context and schema, we pass in the --no-context and --no-schema flags to prevent duplicating what we already have)

This command generates the following files:

lib/chat_api_web/controllers/conversation_controller.ex
lib/chat_api_web/views/conversation_view.ex
test/chat_api_web/controllers/conversation_controller_test.exs
  • lib/chat_api_web/controllers/conversation_controller.ex defines the basic CRUD methods in the controller
  • lib/chat_api_web/views/conversation_view.ex describes the shape of the JSON in the API responses
  • test/chat_api_web/controllers/conversation_controller_test.exs has some autogenerated tests for the controller

The only thing we need to do to get the API endpoints up and running is add this to our router:

ex
# Protected routes
scope "/api", ChatApiWeb do
  pipe_through([:api, :api_protected])

  # ...
  resources("/conversations", ConversationController, except: [:new, :edit])
end

And then update our JSON views to include the associated messages where necessary:

ex
# in conversation_view.ex

defmodule ChatApiWeb.ConversationView do
  use ChatApiWeb, :view
  alias ChatApiWeb.ConversationView
  alias ChatApiWeb.MessageView

  def render("index.json", %{conversations: conversations}) do
    %{data: render_many(conversations, ConversationView, "basic.json")}
  end

  def render("show.json", %{conversation: conversation}) do
    %{data: render_one(conversation, ConversationView, "expanded.json")}
  end

  # Need to distinguish between responses that include the associated messages (in "expanded.json") below
  # vs responses that only include the basic conversation fields without messages (here in "basic.json")
  def render("basic.json", %{conversation: conversation}) do
    %{
      id: conversation.id,
      status: conversation.status
    }
  end

  def render("expanded.json", %{conversation: conversation}) do
    %{
      id: conversation.id,
      status: conversation.status,
      messages: render_many(conversation.messages, MessageView, "message.json")
    }
  end
end

Testing protected routes

The main change we need to make in our tests to account for protected/authenticated routes is adding an authed_conn in the setup:

ex
setup %{conn: conn} do
  user = %ChatApi.Users.User{email: "[email protected]"}
  conn = put_req_header(conn, "accept", "application/json")
  authed_conn = Pow.Plug.assign_current_user(conn, user, [])

  {:ok, conn: conn, authed_conn: authed_conn}
end

(This basically just adds a "current user" to the request context with the pow library, which we use for auth.)