Packages

NebulaGraph client for Elixir — connection pooling, session management, and rich result decoding.

Current section

Files

Jump to
nebula_graph_ex lib mix tasks nebula_graph_ex.gen.graph.ex
Raw

lib/mix/tasks/nebula_graph_ex.gen.graph.ex

defmodule Mix.Tasks.NebulaGraphEx.Gen.Graph do
@shortdoc "Generates a NebulaGraphEx graph module for your application"
@moduledoc """
The recommended way to set up `nebula_graph_ex` in your application.
Generates a `NebulaGraphEx.Graph` module scoped to your OTP app, adds a
default config block to `config/config.exs`, and registers the module in
your application's supervision tree.
## Usage
mix nebula_graph_ex.gen.graph MyApp.Graph
The module name becomes both the Elixir module and the registered connection
pool name. Running the generator:
1. Creates `lib/my_app/graph.ex` — the graph module
2. Inserts a default config block into `config/config.exs`
3. Injects the module as the first child in `lib/my_app/application.ex`
4. Prints a `config/runtime.exs` snippet for secrets
All three file modifications are idempotent — re-running the generator on an
already-configured app skips each step that is already in place.
The file path for the graph module is derived automatically from the module
name. Use `--path` to override it if your project layout differs from the
standard convention:
mix nebula_graph_ex.gen.graph MyApp.Graph --path lib/my_app/connections/graph.ex
## Multiple connections
Run the generator once per graph module:
mix nebula_graph_ex.gen.graph MyApp.PrimaryGraph
mix nebula_graph_ex.gen.graph MyApp.AnalyticsGraph
Each module manages its own connection pool and reads its config from
`Application.get_env(:my_app, MyApp.PrimaryGraph)` independently.
"""
use Mix.Task
@switches [path: :string]
@impl Mix.Task
def run(args) do
{opts, positional, _invalid} = OptionParser.parse(args, strict: @switches)
module_name =
case positional do
[name | _] ->
name
[] ->
Mix.raise("Expected a module name. Usage: mix nebula_graph_ex.gen.graph MyApp.Graph")
end
otp_app = Mix.Project.config()[:app]
file_path = Keyword.get_lazy(opts, :path, fn -> module_to_path(module_name) end)
generate_module(module_name, otp_app, file_path)
inject_config(module_name, otp_app)
inject_supervision(module_name, otp_app)
print_runtime_instructions(module_name, otp_app)
end
# ─── File path derivation ──────────────────────────────────────────────────
defp module_to_path(module_name) do
parts =
module_name
|> String.split(".")
|> Enum.map(&Macro.underscore/1)
"lib/#{Enum.join(parts, "/")}.ex"
end
# ─── Graph module ──────────────────────────────────────────────────────────
defp generate_module(module_name, otp_app, file_path) do
dir = Path.dirname(file_path)
unless File.exists?(dir) do
File.mkdir_p!(dir)
Mix.shell().info([:green, "* creating ", :reset, dir <> "/"])
end
if File.exists?(file_path) do
Mix.shell().info([:yellow, "* skipping ", :reset, file_path, " (already exists)"])
else
File.write!(file_path, module_content(module_name, otp_app))
Mix.shell().info([:green, "* creating ", :reset, file_path])
end
end
defp module_content(module_name, otp_app) do
"""
defmodule #{module_name} do
use NebulaGraphEx.Graph, otp_app: :#{otp_app}
end
"""
end
# ─── config/config.exs ────────────────────────────────────────────────────
defp inject_config(module_name, otp_app) do
path = "config/config.exs"
marker = "config :#{otp_app}, #{module_name}"
cond do
not File.exists?(path) ->
Mix.shell().info([:yellow, "* skipping ", :reset, path, " (file not found)"])
File.read!(path) |> String.contains?(marker) ->
Mix.shell().info([:yellow, "* skipping ", :reset, path, " (config already present)"])
true ->
snippet = """
config :#{otp_app}, #{module_name},
hostname: "localhost",
port: 9669,
username: "root",
pool_size: 10
"""
updated = insert_config_snippet(File.read!(path), snippet)
File.write!(path, updated)
Mix.shell().info([:green, "* updating ", :reset, path])
end
end
defp insert_config_snippet(content, snippet) do
case String.split(content, "\n", trim: false) do
[] ->
String.trim_trailing(snippet) <> "\n"
lines ->
{lines_out, inserted?} =
Enum.flat_map_reduce(lines, false, fn line, inserted? ->
cond do
inserted? ->
{[line], true}
String.match?(line, ~r/^\s*import_config\s+.+$/) ->
{[String.trim_trailing(snippet), "", line], true}
true ->
{[line], false}
end
end)
if inserted? do
Enum.join(lines_out, "\n")
else
String.trim_trailing(content) <> "\n" <> String.trim_trailing(snippet) <> "\n"
end
end
end
# ─── lib/<otp_app>/application.ex ─────────────────────────────────────────
defp inject_supervision(module_name, otp_app) do
path = "lib/#{otp_app}/application.ex"
cond do
not File.exists?(path) ->
Mix.shell().info([:yellow, "* skipping ", :reset, path, " (file not found)"])
File.read!(path) |> String.contains?(module_name) ->
Mix.shell().info([
:yellow,
"* skipping ",
:reset,
path,
" (#{module_name} already present)"
])
true ->
content = File.read!(path)
case insert_child(content, module_name) do
{:ok, updated} ->
File.write!(path, updated)
Mix.shell().info([:green, "* updating ", :reset, path])
:error ->
Mix.shell().info([
:yellow,
"* skipping ",
:reset,
path,
" (could not locate children list — add #{module_name} manually)"
])
end
end
end
# Injects module_name as the first entry in the `children = [...]` list.
# Handles both inline (`children = []`) and multiline (`children = [\n`) forms.
defp insert_child(content, module_name) do
lines = String.split(content, "\n")
{lines_out, done} =
Enum.flat_map_reduce(lines, false, fn line, done ->
if done do
{[line], true}
else
case Regex.run(~r/^(\s*).*children\s*=\s*\[/, line) do
[_, indent] ->
child_indent = indent <> " "
if String.contains?(line, "]") do
# Inline empty list: children = []
new_line =
String.replace(line, "[]", "[\n#{child_indent}#{module_name}\n#{indent}]")
{[new_line], true}
else
# Multiline: children = [
{[line, "#{child_indent}#{module_name},"], true}
end
nil ->
{[line], false}
end
end
end)
if done, do: {:ok, Enum.join(lines_out, "\n")}, else: :error
end
# ─── Runtime secrets reminder ──────────────────────────────────────────────
defp print_runtime_instructions(module_name, otp_app) do
Mix.shell().info("""
To configure secrets at runtime, add the following to config/runtime.exs:
import Config
config :#{otp_app}, #{module_name},
hostname: System.get_env("NEBULA_HOST", "localhost"),
password: fn -> System.fetch_env!("NEBULA_PASS") end
Passing :password as a function ensures it is never captured in compiled
modules or logged in crash reports.
""")
end
end