docs/examples/add_conversations_api.md
These notes outline how I set up a new API endpoint for the conversations resource.
Conversations contextThis 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 modelpriv/repo/migrations/2020xxxx_create_conversations.exs sets up the migration script for the conversations table in the DBlib/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 methodsconversations and messagesAfter 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:
# ...
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:
# 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
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:
def get_conversation!(id) do
Conversation |> Repo.get!(id) |> Repo.preload(:messages)
end
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 controllerlib/chat_api_web/views/conversation_view.ex describes the shape of the JSON in the API responsestest/chat_api_web/controllers/conversation_controller_test.exs has some autogenerated tests for the controllerThe only thing we need to do to get the API endpoints up and running is add this to our router:
# 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:
# 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
The main change we need to make in our tests to account for protected/authenticated routes is adding an authed_conn in the setup:
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.)