0
votes

I gave been following the tutorial made by EQuimper the Instagram clone. I have been trying to extend the application further via the backend as the front end isn't of concern at the moment. I am trying to implement a delete photo feature but it doesn't seem to work and I can't figure out why.

mutation do
    @doc """
    deletes a photo in the database
    """
    field :photo_delete, list_of(:photo) do
        arg :id, non_null(:id)
        resolve &Resolvers.Posts.photo_delete/3
    end
end

My schema.ex contains this code under the mutations section.

def photos(_,_,_) do
    {:ok, Instagram.Posts.list_photos}
end

def photo(_, %{id: id}, _) do
    {:ok, Instagram.Posts.get_photo!(id)}
end

def photo_delete(_, %{id: id}, _) do
    photo_from_db = {:ok, Instagram.Posts.get_photo!(id)}
    {:ok, Instagram.Posts.delete_photo(photo_from_db)}
end

This is the code in the resolver.posts file which for returning a list or a single photo works.

def delete_photo(%Photo{} = photo) do
Repo.delete(photo)
end

This is the code that executes the mutation to delete the photo from the database, which takes in a photo struct and deletes it.

object :photo do
    field :id, non_null(:id)
    field :image_url, non_null(:string)
    field :caption, :string

    field :inserted_at, non_null(:string)
    field :update_at, non_null(:string)
end

This is the code that defines the photo schema.

schema "photos" do
field :caption, :string
field :image_url, :string

timestamps()

end

@doc false def changeset(%Photo{} = photo, attrs) do photo |> cast(attrs, [:image_url, :caption]) |> validate_required([:image_url]) end

this code is in the photo.ex file that handles the schema (I think)

 mutation {
  photo_delete(id: 1){
    id
 }
}

this is the mutation that I run to delete the query from the database. It returns an error saying

"no function clause matching in Instagram.Posts.delete_photo/1"

returned from the terminal. What have I done wrong? and what do I not understand about the flow of functions in this example. Link to the video series: https://www.youtube.com/watch?v=AVQQF_J3Az0&list=PLzQWIQOqeUSOu74jGJMRH06gneM3wL82Z for further clarification.

3

3 Answers

0
votes

I think the most likely culprit is the line right above your function call in schema.ex:

photo_from_db = {:ok, Instagram.Posts.get_photo!(id)}

What you're probably wanting is this:

{:ok, photo_from_db} = Instagram.Posts.get_photo!(id)

That way you'll pass the function the photo struct that it's expecting, instead of {:ok, %Photo{}}.

0
votes

Found the answer

field :delete_user, :user do
        arg :id, non_null(:id)
        resolve &Graphical.UserResolver.delete/2
    end

this goes in your schema.ex file

 def delete(%{id: id}, _info) do
    Accounts.get_user!(id)
    |> Accounts.delete_user
end

this then gets called from the schema file, which will find the record at that id then pipe it to the delete method where it will be removed from the database

0
votes
def photo_delete(_, %{id: id}, _) do
    photo_from_db = {:ok, Instagram.Posts.get_photo!(id)}
    {:ok, Instagram.Posts.delete_photo(photo_from_db)}
end

should be something like

with {:ok, photo_from_db} <- Instagram.Posts.get_photo!(id) do
  Instagram.Posts.delete_photo(photo_from_db)
else 
  {:error, error} ->
    {:error, error}
end

or something similar.

Also, I now write all of my context functions to return :ok/:error tuples so everything plays nice with Absinthe. Seems like a good practice in general as well.

By hardcoding :ok conditions you're not properly handling failing cases and are just going to throw exceptions instead of returning useful errors.

You can use this kind of middleware for handling errors:

defmodule ApiWeb.Middleware.ErrorMiddleware do
  require Logger
  alias Api.Error.AbsintheError

  def add_error_handling(spec) do
    fn res, config ->
      spec
      |> to_fun(res, config)
      |> exec_safely(res)
    end
  end

  defp to_fun({{module, function}, config}, res, _config) do
    fn -> apply(module, function, [res, config]) end
  end

  defp to_fun({module, config}, res, _config) do
    fn -> apply(module, :call, [res, config]) end
  end

  defp to_fun(module, res, config) when is_atom(module) do
    fn -> apply(module, :call, [res, config]) end
  end

  defp to_fun(fun, res, config) when is_function(fun, 2) do
    fn -> fun.(res, config) end
  end

  defp exec_safely(fun, res) do
    fun.()
    |> Map.update!(:errors, &Enum.map(&1, fn e -> AbsintheError.serialize(e) end))
  rescue
    err ->
      # TODO: https://authkit.atlassian.net/projects/AUT/issues/AUT-9
      Logger.error(Exception.format(:error, err, __STACKTRACE__))

      Absinthe.Resolution.put_result(
        res,
        {:error, %{code: :internal_server_error, message: "An internal server error has occured"}}
      )
  end
end

and then build error structs like this

defmodule Api.Error.NotFoundError do
  @type error_source ::
          :internal | :network

  @type t :: %__MODULE__{
          source: error_source,
          code: :not_found,
          message: String.t()
        }

  @enforce_keys [:source, :code, :message]
  defstruct [:source, :code, :message]

  @spec new(Keyword.t()) :: t
  def new(fields) do
    struct!(__MODULE__, fields)
  end
end

and implement like this

defprotocol Api.Error.AbsintheError do
  def serialize(err)
end

defimpl Api.Error.AbsintheError, for: Api.Error.NotFoundError do
  @doc """
  `serialize` takes our standard `%NotFoundError{}` struct and converts it
  into a regular map in order to make it play nice with `Absinthe`.  We then
  use `Absinthe` `middleware` to force the execution of serialize every time
  it gets passed a `%NotFoundError{}`.
  """
  def serialize(err) do
    %{source: err.source, code: err.code, message: err.message}
  end
end