Packages

Neo4j driver for Elixir, using the fast Bolt protocol

Current section

Files

Jump to
boltx lib bolt_sips response.ex
Raw

lib/bolt_sips/response.ex

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