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