defmodule Bolt.Sips.Response do @moduledoc """ Support for transforming a Bolt response to a list of Bolt.Sips.Types or arbitrary values. A Bolt.Sips.Response is used for mapping any response received from a Neo4j server into a an Elixir struct. You'll interact with this module every time you're needing to get and manipulate the data resulting from your queries. For example, a simple Cypher query like this: Bolt.Sips.query(Bolt.Sips.conn(), "RETURN [10,11,21] AS arr") will return: {:ok, %Bolt.Sips.Response{ bookmark: "neo4j:bookmark:v1:tx21868", fields: ["arr"], notifications: [], plan: nil, profile: nil, records: [[[10, 11, 21]]], results: [%{"arr" => [10, 11, 21]}], stats: [], type: "r" }} and while you have now access to the full data returned by Neo4j, most of the times you'll just want the results: iex> %Bolt.Sips.Response{results: results} = Bolt.Sips.query!(Bolt.Sips.conn(), "RETURN [10,11,21] AS arr") iex> results [%{"arr" => [10, 11, 21]}] More complex queries, i.e.: MATCH p=({name:'Alice'})-[r:KNOWS]->({name:'Bob'}) RETURN r will return `results` like this: [%{"r" => %Bolt.Sips.Types.Relationship{end: 647, id: 495, properties: %{}, start: 646, type: "KNOWS"}}] Note: the `Path` has also functionality for "drawing" a graph, from a given node-relationship path Our Bolt.Sips.Response is implementing Elixir's [Enumerable Protocol](https://hexdocs.pm/elixir/Enumerable.html), to help you accessing the results. Hence something like this, is possible: iex> Bolt.Sips.query!(Bolt.Sips.conn(), "RETURN [10,11,21] AS arr") |> ...> Enum.reduce(0, &(Enum.sum(&1["arr"]) + &2)) 42 an overly complicated example, but you get the point?! :) You can also quickly get the `first` of the results, returned by Neo4j. Example: iex> Bolt.Sips.query!(Bolt.Sips.conn(), "UNWIND range(1, 10) AS n RETURN n") |> ...> Bolt.Sips.Response.first() %{"n" => 1} """ @type t :: %__MODULE__{ results: list, fields: list, records: list, plan: map, notifications: list, stats: list | map, profile: any, type: String.t(), bookmark: String.t() } @type key :: any @type value :: any @type acc :: any @type element :: any defstruct results: [], fields: nil, records: [], plan: nil, notifications: [], stats: [], profile: nil, type: nil, bookmark: nil alias Bolt.Sips.Error require Logger require Integer def first(%__MODULE__{results: []}), do: nil def first(%__MODULE__{results: [head | _tail]}), do: head @doc false # transform a raw Bolt response to a list of Responses def transform!(records, stats \\ :no) do with {:ok, %__MODULE__{} = records} <- transform(records, stats) do records else e -> e end end def transform(records, _stats \\ :no) do # records |> IO.inspect(label: "raw>> lib/bolt_sips/response.ex:44") with %__MODULE__{fields: fields, records: records} = response <- parse(records) do {:ok, %__MODULE__{response | results: create_results(fields, records)}} else e -> {:error, e} end rescue e in Bolt.Sips.Exception -> {:error, e.message} e -> {:error, e} end def fetch(%Bolt.Sips.Response{fields: fields, results: results}, key) do with true <- Enum.member?(fields, key) do findings = Enum.map(results, fn map -> Map.get(map, key) end) |> Enum.filter(&(&1 != nil)) {:ok, findings} else _ -> nil end end def fetch!(%Bolt.Sips.Response{} = r, key) do with {:ok, findings} <- fetch(r, key) do findings end end defp parse(records) do with {err_type, error} when err_type in ~w(halt error failure)a <- Error.new(records) do {:error, error} else records -> response = records |> Enum.reduce(%__MODULE__{}, fn {type, record}, acc -> with {:error, e} <- parse_record(type, record, acc) do raise Bolt.Sips.Exception, e else acc -> acc end end) %__MODULE__{response | records: response.records |> :lists.reverse()} end end # defp parse_record(:success, %{"fields" => fields, "t_first" => 0}, response) do defp parse_record(:success, %{"fields" => fields}, response) do %{response | fields: fields} end defp parse_record(:success, %{"profile" => profile, "type" => type} = record, response) do %{response | profile: profile, stats: Map.get(record, "stats", []), type: type} end defp parse_record(:success, %{"notifications" => n, "plan" => plan, "type" => type}, response) do %{response | plan: plan, notifications: n, type: type} end defp parse_record(:success, %{"plan" => plan, "type" => type}, response) do %{response | plan: plan, type: type} end defp parse_record(:success, %{"stats" => stats, "type" => type}, response) do %{response | stats: stats, type: type} end defp parse_record(:success, %{"bookmark" => bookmark, "type" => type}, response) do %{response | bookmark: bookmark, type: type} end defp parse_record(:success, %{"type" => type}, response) do %{response | type: type} end defp parse_record(:success, %{}, response) do %{response | type: "boltkit?"} end defp parse_record(:success, record, _response) do line = "; around: #{String.replace_leading("#{__ENV__.file}", "#{File.cwd!()}", "") |> Path.relative()}:#{__ENV__.line()}" err_msg = "UNKNOWN success type: " <> inspect(record) <> line Logger.error(err_msg) {:error, Bolt.Sips.Error.new(err_msg)} end # defp parse_record(:record, %{"bookmark" => "neo4j:bookmark:v1:tx14519", "t_last" => 1, "type" => "r"}, response) do defp parse_record(:record, record, response) do %{response | records: [record | response.records]} end defp parse_record(_type, record, _response) do line = "; around: #{String.replace_leading("#{__ENV__.file}", "#{File.cwd!()}", "") |> Path.relative()}:#{__ENV__.line()}" err_msg = "UNKNOWN `:record`: " <> inspect(record) <> line Logger.error(err_msg) {:error, Bolt.Sips.Error.new(err_msg)} end defp create_results(fields, records) do records |> Enum.map(fn recs -> Enum.zip(fields, recs) end) |> Enum.map(fn data -> Enum.into(data, %{}) end) end end