Powered by AppSignal & Oban Pro

terminusdb_ex Livebook Demo

guides/terminusdb_ex_livebook.livemd

terminusdb_ex Livebook Demo

Mix.install([
  {:terminusdb_client, "~> 0.3.3"}
])

Setup

Start a TerminusDB server first:

docker run -d --name terminusdb -p 6363:6363 -e TERMINUSDB_ADMIN_PASS=root terminusdb/terminusdb-server:latest

Then wait for it to be ready:

endpoint = "http://localhost:6363"

# Wait for the server to be ready
for _ <- 1..30 do
  case Req.get("#{endpoint}/api/ok") do
    {:ok, %{status: 200}} -> :ok
    _ -> Process.sleep(1000)
  end
end
[:ok, :ok, :ok, :ok, :ok, :ok, :ok, :ok, :ok, :ok, :ok, :ok, :ok, :ok, :ok, :ok, :ok, :ok, :ok, :ok,
 :ok, :ok, :ok, :ok, :ok, :ok, :ok, :ok, :ok, :ok]

1. Configuration

alias TerminusDB.{Config, Database, Document, Schema, Branch, Client}

config = Config.new(endpoint: endpoint)
%TerminusDB.Config{
  endpoint: "http://localhost:6363",
  token: nil,
  user: "admin",
  key: "root",
  organization: "admin",
  database: nil,
  branch: "main",
  repo: "local",
  ref: nil,
  adapter: nil,
  headers: %{},
  receive_timeout: 15000,
  telemetry: true,
  user_agent: "terminusdb_ex/0.3.3"
}
# Inspect auth
Config.auth(config)
{:basic, "admin:root"}
# Redact secrets for safe logging
Config.redact(config)
%TerminusDB.Config{
  endpoint: "http://localhost:6363",
  token: nil,
  user: "admin",
  key: "[redacted]",
  organization: "admin",
  database: nil,
  branch: "main",
  repo: "local",
  ref: nil,
  adapter: nil,
  headers: %{},
  receive_timeout: 15000,
  telemetry: true,
  user_agent: "terminusdb_ex/0.3.3"
}

2. Database management

# Create a database with a schema graph
db_name = "livebook_demo"
  if Database.exists?(config, db_name) do
  {:ok, _} = Database.delete(config, db_name, force: true)
end

{:ok, _} =
  Database.create(config, db_name,
    label: "Livebook Demo",
    comment: "A database for the livebook walkthrough",
    schema: true
  )
{:ok,
 %{
   "@type" => "api:DbCreateResponse",
   "api:database_uri" => "terminusdb://system/data/UserDatabase/SmeaEIKVI6YM7zn3",
   "api:status" => "api:success"
 }}
# Check it exists
Database.exists?(config, db_name)
true
# List all databases
{:ok, dbs} = Database.list(config)
{:ok, [%{"path" => "admin/livebook_demo-tester"}, %{"path" => "admin/livebook_demo"}]}

3. Document operations

Scope the config to the database:

config = Config.with_database(config, db_name)
%TerminusDB.Config{
  endpoint: "http://localhost:6363",
  token: nil,
  user: "admin",
  key: "root",
  organization: "admin",
  database: "livebook_demo",
  branch: "main",
  repo: "local",
  ref: nil,
  adapter: nil,
  headers: %{},
  receive_timeout: 15000,
  telemetry: true,
  user_agent: "terminusdb_ex/0.3.3"
}

Insert a schema

{:ok, _} =
  Document.insert(config,
    %{
      "@type" => "Class",
      "@id" => "Person",
      "name" => "xsd:string",
      "age" => "xsd:integer",
      "email" => "xsd:string"
    },
    author: "admin",
    message: "Add Person schema",
    graph_type: :schema
  )
{:ok, ["Person"]}

Insert documents

{:ok, [alice_id|_]} =
  Document.insert(config,
    %{"@type" => "Person", "name" => "Alice", "age" => 30, "email" => "alice@example.com"},
    author: "admin",
    message: "Add Alice"
  )
{:ok, ["terminusdb:///data/Person/LPnwAEvaeyRJngy2"]}
{:ok, [bobs_id|_]} =
  Document.insert(config, [
    %{"@type" => "Person", "name" => "Bob", "age" => 25, "email" => "bob@example.com"},
    %{"@type" => "Person", "name" => "Carol", "age" => 28, "email" => "carol@example.com"}
  ], author: "admin", message: "Add Bob and Carol")
{:ok, ["terminusdb:///data/Person/mWqqWA7AmhotGfCe", "terminusdb:///data/Person/-9MoNh4h976kbIRe"]}

Retrieve documents

{:ok, docs} = Document.get(config, type: "Person", as_list: true)
Enum.map(docs, & &1["name"])
["Alice", "Alice", "Carol", "Bob"]
{:ok, matches} =
  Document.query(config, %{"@type" => "Person", "age" => 28})

Enum.map(matches, & &1["name"])
["Carol"]
{:ok, person} = Document.get(config, id: alice_id, as_list: false)

{:ok, _} =
  Document.replace(config,
    Map.put(person, "age", 31),
    author: "admin",
    message: "Happy birthday Alice"
  )

{:ok, _updated_person} = Document.get(config, id: alice_id, as_list: false)
{:ok,
 %{
   "@id" => "Person/LPnwAEvaeyRJngy2",
   "@type" => "Person",
   "age" => 31,
   "email" => "alice@example.com",
   "name" => "Alice"
 }}

Delete a document

{:ok, _} = Document.delete(config,
  id: bobs_id,
  author: "admin",
  message: "Remove Bob"
)
{:ok, ""}

4. Schema frames

{:ok, _frame} = Schema.frame(config, "Person")
{:ok,
 %{"@type" => "Class", "age" => "xsd:integer", "email" => "xsd:string", "name" => "xsd:string"}}
{:ok, all} = Schema.all(config)
Map.keys(all)
["Person"]

5. Branches

# Create a branch
{:ok, _} = Branch.create(config, "feature-x")
{:ok, %{"@type" => "api:BranchResponse", "api:status" => "api:success"}}
# It exists
Branch.exists?(config, "feature-x")
true
# Switch to it
feature_config = Config.with_branch(config, "feature-x")

# Insert on the branch
{:ok, _} =
  Document.insert(feature_config,
    %{"@type" => "Person", "name" => "Dave", "age" => 40, "email" => "dave@example.com"},
    author: "admin",
    message: "Add Dave on feature branch"
  )
{:ok, ["terminusdb:///data/Person/5vU__VxlSRGxjZtH"]}
# Dave is on the feature branch
{:ok, feature_docs} = Document.get(feature_config, type: "Person", as_list: true)
"Dave" in Enum.map(feature_docs, & &1["name"])
true
# Dave is NOT on the main branch
{:ok, main_docs} = Document.get(config, type: "Person", as_list: true)
"Dave" in Enum.map(main_docs, & &1["name"])
false
# Delete the branch
{:ok, _} = Branch.delete(config, "feature-x")
{:ok, %{"@type" => "api:BranchResponse", "api:status" => "api:success"}}

6. Streaming

# Stream documents one at a time (constant memory for large result sets)
Document.stream(config, type: "Person")
|> Stream.map(& &1["name"])
|> Enum.to_list()
["Alice", "Alice", "Carol"]

7. Telemetry

:telemetry.attach_many(
  "livebook-telemetry",
  [[:terminusdb, :document, :stop], [:terminusdb, :database, :stop]],
  fn _event, measurements, meta, _ctx ->
    duration_ms = System.convert_time_unit(measurements[:duration] || 0, :native, :millisecond)
    IO.puts("[#{meta.area}] #{meta.method} #{meta.path} -> #{meta.status} (#{duration_ms}ms)")
  end,
  nil
)

19:30:10.866 [info] The function passed as a handler with ID "livebook-telemetry" is a local function.
This means that it is either an anonymous function or a capture of a function without a module specified. That may cause a performance penalty when calling that handler. For more details see the note in `telemetry:attach/4` documentation.

https://hexdocs.pm/telemetry/telemetry.html#attach/4
:ok
# Now operations emit telemetry events
{:ok, _} = Document.insert(config,
  %{"@type" => "Person", "name" => "Eve", "age" => 35, "email" => "eve@example.com"},
  author: "admin",
  message: "Add Eve"
)
[document] post document/admin/livebook_demo/local/branch/main -> 200 (34ms)
{:ok, ["terminusdb:///data/Person/IntZyco3cFStgR1I"]}
 [document] :post document/admin/livebook_demo -> 200 (15ms)
:telemetry.detach("livebook-telemetry")
:ok

8. Error handling

# Tuple-returning (non-raising)
case Database.create(config, "livebook_demo", label: "Duplicate") do
  {:ok, _} ->
    IO.puts("Created (unexpected)")

  {:error, %{reason: :api, api_type: "api:DatabaseAlreadyExists"} = e} ->
    IO.puts("Expected error: #{Exception.message(e)}")

  {:error, %{reason: :api} = e} ->
    IO.puts("Other API error: #{Exception.message(e)}")

  {:error, e} ->
    IO.puts("Other error: #{inspect(e)}")
end
Expected error: TerminusDB API error 400 (api:DatabaseAlreadyExists): Database already exists.
:ok
# Raising variant
try do
  Database.create!(config, "livebook_demo", label: "Duplicate")
rescue
  e in TerminusDB.Error ->
    IO.puts("Raised as expected: #{Exception.message(e)}")
end
Raised as expected: TerminusDB API error 400 (api:DatabaseAlreadyExists): Database already exists.
:ok

9. Raw client

# Direct access to any endpoint
{:ok, info} = Client.request(config, :get, "info")
info["api:info"]["terminusdb"]["version"]
"12.0.6"

Cleanup

{:ok, _} = Database.delete(config, "livebook_demo", force: true)
{:ok, %{"@type" => "api:DbDeleteResponse", "api:status" => "api:success"}}
# Stop the container
# System.cmd("docker", ["stop", "terminusdb"])
# System.cmd("docker", ["rm", "terminusdb"])