Current section
Files
Jump to
Current section
Files
lib/bolt_sips.ex
defmodule Bolt.Sips do
@moduledoc """
A Neo4j driver for Elixir providing many useful features:
- using the Bolt protocol, the Elixir implementation - the Neo4j's newest network protocol, designed for high-performance; latest Bolt versions, are supported.
- Can connect to a standalone Neo4j server (`:direct` mode) or to a Neo4j causal cluster, using the `bolt+routing` or the newer `neo4j` schemes; connecting in `:routing` mode.
- Provides the user with the ability to create and manage distinct ad-hoc `role-based` connections to one or more Neo4j servers/databases
- Supports transactions, simple and complex Cypher queries with or w/o parameters
- Multi-tenancy
- Supports Neo4j versions: 3.0.x/3.1.x/3.2.x/3.4.x/3.5.x
To start, add the `:bolt_sips` dependency to you project, run `mix do deps.get, compile` on it and then you can quickly start experimenting with Neo4j from the convenience of your IEx shell. Example:
iex> {:ok, _neo} = Bolt.Sips.start_link(url: "bolt://neo4j:test@localhost")
{:ok, #PID<0.250.0>}
iex> conn = Bolt.Sips.conn()
#PID<0.256.0>
iex> Bolt.Sips.query!(conn, "RETURN 1 as n")
%Bolt.Sips.Response{
records: [[1]],
results: [%{"n" => 1}]
}
the example above presumes that you have a Neo4j server available locally, using the Bolt protocol and requiring authentication.
"""
use Supervisor
@registry_name :bolt_sips_registry
# @timeout 15_000
# @max_rows 500
alias Bolt.Sips.{Query, ConnectionSupervisor, Router, Error, Response, Exception}
@type conn :: DBConnection.conn()
@type transaction :: DBConnection.t()
@doc """
Start or add a new Neo4j connection
## Options:
- `:url`- a full url to pointing to a running Neo4j server. Please remember you must specify the scheme used to connect to the server. Valid schemes:`bolt`,`bolt+routing`and`neo4j` - the last two being used for connecting to a Neo4j causal cluster.
- `:pool_size` - the size of the connection pool. Default: 15
- `:timeout` - a connection timeout value defined in milliseconds. Default: 15_000
- `:ssl`-`true`, if the connection must be encrypted. Default:`false`
- `:prefix`- used for differentiating between multiple connections available in the same app. Default:`:default`
## Example of valid configurations (i.e. defined in config/dev.exs) and usage:
This is the most basic configuration:
config :bolt_sips, Bolt,
url: "bolt://localhost:7687"
and if you need to connect to remote servers:
config :bolt_sips, Bolt,
url: "bolt://Bilbo:Baggins@hobby-hobbits.dbs.graphenedb.com:24786",
ssl: true,
timeout: 15_000,
Example with a configuration defined in the `config/dev.exs`:
opts = Application.get_env(:bolt_sips, Bolt)
{:ok, pid} = Bolt.Sips.start_link(opts)
Bolt.Sips.query!(pid, "CREATE (a:Person {name:'Bob'})")
Bolt.Sips.query!(pid, "MATCH (a:Person) RETURN a.name AS name")
|> Enum.map(&(&1["name"]))
Or defining an ad-hoc configuration:
Example with a configuration defined in the `config/dev.exs`:
{:ok, _neo} = Bolt.Sips.start_link(url: "bolt://neo4j:test@localhost")
conn = Bolt.Sips.conn()
Bolt.Sips.query!(conn, "return 1 as n")
"""
@spec start_link(Keyword.t()) :: Supervisor.on_start()
def start_link(opts) do
with nil <- Process.whereis(__MODULE__) do
Supervisor.start_link(__MODULE__, opts, name: __MODULE__)
else
pid ->
Router.configure(opts)
{:ok, pid}
{pid, node} ->
Router.configure(opts)
{:ok, {pid, node}}
end
end
@doc false
def init(opts) do
[
Registry.child_spec(keys: :unique, name: registry_name()),
ConnectionSupervisor,
{Router, opts}
]
|> Supervisor.init(strategy: :one_for_one)
end
@doc """
Returns a pool name which can be used to acquire a connection from a pool of servers
responsible with a specific type of operations: read, write and route, or all of the above: "direct"
"""
@spec conn(atom, keyword) :: conn
def conn(role \\ :direct, opts \\ [prefix: :default])
def conn(role, opts) do
prefix = Keyword.get(opts, :prefix, :default)
with {:ok, conn} <- Router.get_connection(role, prefix) do
conn
else
{:error, e} ->
raise Exception, e
e ->
{:error, e}
end
end
## Query
########################
@doc """
sends the query (and its parameters) to the server and returns `{:ok, Response.t()}` or
`{:error, Error}` otherwise
"""
@spec query(conn, String.t()) :: {:ok, Response.t() | [Response.t()]} | {:error, Error.t()}
defdelegate query(conn, statement), to: Query
@doc """
The same as query/2 but raises a Exception if it fails.
Returns the server response otherwise.
"""
@spec query!(conn, String.t()) :: Response.t() | [Response.t()] | Exception.t()
defdelegate query!(conn, statement), to: Query
@doc """
send a query and an associated map of parameters. Returns the server response or an error
"""
@spec query(conn, String.t(), map()) ::
{:ok, Response.t() | [Response.t()]} | {:error, Error.t()}
defdelegate query(conn, statement, params), to: Query
@doc """
The same as query/3 but raises a Exception if it fails.
"""
@spec query!(conn, String.t(), map()) :: Response.t() | [Response.t()] | Exception.t()
defdelegate query!(conn, statement, params), to: Query
@doc """
send a query and an associated map of parameters with options. Returns the server response or an error
The `opts` keyword list will be passed to `DbConnection.execute/4`.
By default, the query will timeout after `15_000` milliseconds, this may be overriden by setting the `timeout`
option in `opts`.
"""
@spec query(conn, String.t(), map(), Keyword.t()) ::
{:ok, Response.t() | [Response.t()]} | {:error, Error.t()}
defdelegate query(conn, statement, params, opts), to: Query
@doc """
The same as query/4 but raises a Exception if it fails.
"""
@spec query!(conn, String.t(), map(), Keyword.t()) ::
Response.t() | [Response.t()] | Exception.t()
defdelegate query!(conn, statement, params, opts), to: Query
## Transaction
########################
@doc """
Example:
```elixir
setup do
{:ok, [main_conn: Bolt.Sips.conn()]}
end
test "execute statements in transaction", %{main_conn: main_conn} do
Bolt.Sips.transaction(main_conn, fn conn ->
book =
Bolt.Sips.query!(conn, "CREATE (b:Book {title: \"The Game Of Trolls\"}) return b")
|> Response.first()
assert %{"b" => g_o_t} = book
assert g_o_t.properties["title"] == "The Game Of Trolls"
Bolt.Sips.rollback(conn, :changed_my_mind)
end)
books = Bolt.Sips.query!(main_conn, "MATCH (b:Book {title: \"The Game Of Trolls\"}) return b")
assert Enum.count(books) == 0
end
```
"""
defdelegate transaction(conn, fun, opts \\ []), to: DBConnection
@doc """
Rollback a database transaction and release lock on connection.
When inside of a `transaction/3` call does a non-local return, using a
`throw/1` to cause the transaction to enter a failed state and the
`transaction/3` call returns `{:error, reason}`. If `transaction/3` calls are
nested the connection is marked as failed until the outermost transaction call
does the database rollback.
### Example
{:error, :oops} = Bolt.Sips.transaction(pool, fn(conn) ->
Bolt.Sips.rollback(conn, :oops)
end)
"""
@spec rollback(DBConnection.t(), reason :: any) :: no_return
defdelegate rollback(conn, opts), to: DBConnection
@doc """
terminate a pool of connections with the role specified
"""
defdelegate terminate_connections(role), to: Router
@doc """
peek into the main Router state, and return the internal state controlling the connections
to the server/server. Mostly for internal use or for helping driver developers.
The authentication credentials will be sanitized, if any
### Examples:
iex> Bolt.Sips.info()
%{default: %{connections: %{direct: %{"localhost:7687" => 0}, routing_query: nil, zorba: %{"localhost:7687" => 0}}, user_options: [basic_auth: [username: "******", password: "******"], socket: Bolt.Sips.Socket, port: 7687, routing_context: %{}, schema: "bolt", hostname: "localhost", timeout: 15000, ssl: false, with_etls: false, prefix: :default, url: "bolt://localhost", pool_size: 10, max_overflow: 2, role: :zorba]}}
"""
@spec info() :: map
def info(), do: sanitized_info(Bolt.Sips.Router.info())
@doc """
extract the routing table from the router
"""
@spec routing_table(any) :: map
def routing_table(prefix \\ :default)
def routing_table(prefix) do
Bolt.Sips.Router.routing_table(prefix)
end
@doc """
the registry name used across the various driver components
"""
@spec registry_name() :: :bolt_sips_registry
def registry_name(), do: @registry_name
@hide_auth [username: "******", password: "******"]
defp sanitized_info(info) when is_map(info) do
for {k, v} <- info, into: %{} do
{k, Map.update(v, :user_options, @hide_auth, &Keyword.put(&1, :basic_auth, @hide_auth))}
end
end
defp sanitized_info(info), do: info
end