Skip to content

A web server that offers an HTTP/JSON API for interacting with Datomic databases.

License

Notifications You must be signed in to change notification settings

gbaptista/datomic-flare

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

10 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Flare

A web server that offers an HTTP/JSON API for interacting with Datomic databases.

The image features a logo with curved lines forming a tesseract, suggesting distortion and movement like space-time.

This is not an official Datomic project or documentation and it is not affiliated with Datomic in any way.

TL;DR and Quick Start

Ensure you have Java and Clojure installed.

cp .env.example .env
clj -M:run
[main] INFO flare.components.server - Starting server on http://0.0.0.0:3042 as peer

Ensure you have curl, bb, and jq installed.

Transact a Schema:

echo '
[{:db/ident       :book/title
  :db/valueType   :db.type/string
  :db/cardinality :db.cardinality/one
  :db/doc         "The title of the book."}

 {:db/ident       :book/genre
  :db/valueType   :db.type/string
  :db/cardinality :db.cardinality/one
  :db/doc         "The genre of the book."}]
' \
| bb -e '(pr-str (edn/read-string (slurp *in*)))' \
| curl -s http://localhost:3042/datomic/transact \
  -X POST \
  -H "Content-Type: application/json" \
  --data-binary @- <<JSON \
| jq
{
  "data": $(cat)
}
JSON

Assert a Fact:

echo '
[{:db/id      -1
  :book/title "The Tell-Tale Heart"
  :book/genre "Horror"}]
' \
| bb -e '(pr-str (edn/read-string (slurp *in*)))' \
| curl -s http://localhost:3042/datomic/transact \
  -X POST \
  -H "Content-Type: application/json" \
  --data-binary @- <<JSON \
| jq
{
  "data": $(cat)
}
JSON

Read the Data by Querying:

echo '
[:find ?e ?title ?genre
 :where [?e :book/title ?title]
        [?e :book/genre ?genre]]
' \
| bb -e '(pr-str (edn/read-string (slurp *in*)))' \
| curl -s http://localhost:3042/datomic/q \
  -X GET \
  -H "Content-Type: application/json" \
  --data-binary @- <<JSON \
| jq
{
  "inputs": [
    {
      "database": {
        "latest": true
      }
    }
  ],
  "query": $(cat)
}
JSON
{
  "data": [
    [
      4611681620380877802,
      "The Tell-Tale Heart",
      "Horror"
    ]
  ]
}

Setup

Running Datomic

You can set up Datomic by following its official documentation: Pro Setup.

Alternatively, if you have Docker, you can leverage datomic-pro-docker.

Clone the repository and copy the Docker Compose template:

git clone https://github.com/gbaptista/datomic-pro-docker.git

cd datomic-pro-docker

cp compose/datomic-postgresql.yml docker-compose.yml

Start PostgreSQL as Datomic's storage service:

docker compose up -d datomic-storage

docker compose logs -f datomic-storage

Create the table for Datomic databases:

docker compose run datomic-tools psql \
  -h datomic-storage \
  -U datomic-user \
  -d my-datomic-storage \
  -c 'CREATE TABLE datomic_kvs (
        id TEXT NOT NULL,
        rev INTEGER,
        map TEXT,
        val BYTEA,
        CONSTRAINT pk_id PRIMARY KEY (id)
      );'

You will be prompted for a password, which is unsafe.

Start the Datomic Transactor:

docker compose up -d datomic-transactor

docker compose logs -f datomic-transactor

Create your database named my-datomic-database:

docker compose run datomic-tools clojure -M -e "$(cat <<'CLOJURE'
  (require '[datomic.api :as d])
  (def db-uri "datomic:sql://my-datomic-database?jdbc:postgresql://datomic-storage:5432/my-datomic-storage?user=datomic-user&password=unsafe")
  (d/create-database db-uri)
  (System/exit 0)
CLOJURE
)"

Your Datomic database is ready to start using Flare in Peer Mode:

graph RL
    Transactor --- Storage[(Storage)]
    Flare("Flare (Peer)") -.- Storage
    Flare --- Transactor
    Application([Application]) --- Flare
Loading

If you want to use Client Mode, start a Datomic Peer Server:

docker compose up -d datomic-peer-server

docker compose logs -f datomic-peer-server

Your Datomic database is ready to start using Flare in Client Mode:

graph RL
    Transactor --- Storage[(Storage)]
    PeerServer --- Transactor
    PeerServer -.- Storage
    Flare(Flare) --- PeerServer[Peer Server]
    Application([Application]) --- Flare
Loading

Running Flare

With Datomic running, you are ready to run Flare:

git clone https://github.com/gbaptista/datomic-flare.git

cd datomic-flare

The server can operate in Peer Mode, embedding com.datomic/peer to establish a Peer directly within the server, or in Client Mode, using com.datomic/client-pro to connect to a Datomic Peer Server.

Copy the .env.example file and fill it with the appropriate information.

cp .env.example .env

If you want Peer Mode:

FLARE_PORT=3042
FLARE_BIND=0.0.0.0

FLARE_MODE=peer

FLARE_PEER_CONNECTION_URI="datomic:sql://my-datomic-database?jdbc:postgresql://localhost:5432/my-datomic-storage?user=datomic-user&password=unsafe"

If you want Client Mode:

FLARE_PORT=3042
FLARE_BIND=0.0.0.0

FLARE_MODE=client

FLARE_CLIENT_ENDPOINT=localhost:8998
FLARE_CLIENT_SECRET=unsafe-secret
FLARE_CLIENT_ACCESS_KEY=unsafe-key
FLARE_CLIENT_DATABASE_NAME=my-datomic-database

Ensure you have Java and Clojure installed.

Run the server:

clj -M:run
[main] INFO flare.components.server - Starting server on http://0.0.0.0:3042 as peer

Ensure you have curl, bb, and jq installed, and you should be able to start firing requests to the server:

curl -s http://localhost:3042/meta \
  -X GET \
  -H "Content-Type: application/json"  \
| jq
{
  "data": {
    "mode": "peer",
    "datomic-flare": "1.0.0",
    "org.clojure/clojure": "1.12.0",
    "com.datomic/peer": "1.0.7187",
    "com.datomic/client-pro": "1.0.81"
  }
}

Quick Start

Clojure

If you are using Clojure, you already have native access to Datomic APIs and probably should not be using Flare.

Java

If you are using Java, you already have native access to Datomic APIs and probably should not be using Flare.

Bash

Ensure you have curl, bb, and jq installed.

bash flare.sh
echo '
[{:db/ident       :book/title
  :db/valueType   :db.type/string
  :db/cardinality :db.cardinality/one
  :db/doc         "The title of the book."}

 {:db/ident       :book/genre
  :db/valueType   :db.type/string
  :db/cardinality :db.cardinality/one
  :db/doc         "The genre of the book."}]
' \
| bb -e '(pr-str (edn/read-string (slurp *in*)))' \
| curl -s http://localhost:3042/datomic/transact \
  -X POST \
  -H "Content-Type: application/json" \
  --data-binary @- <<JSON \
| jq
{
  "data": $(cat)
}
JSON

echo '
[{:db/id      -1
  :book/title "The Tell-Tale Heart"
  :book/genre "Horror"}]
' \
| bb -e '(pr-str (edn/read-string (slurp *in*)))' \
| curl -s http://localhost:3042/datomic/transact \
  -X POST \
  -H "Content-Type: application/json" \
  --data-binary @- <<JSON \
| jq
{
  "data": $(cat)
}
JSON

echo '
[:find ?e ?title ?genre
 :where [?e :book/title ?title]
        [?e :book/genre ?genre]]
' \
| bb -e '(pr-str (edn/read-string (slurp *in*)))' \
| curl -s http://localhost:3042/datomic/q \
  -X GET \
  -H "Content-Type: application/json" \
  --data-binary @- <<JSON \
| jq
{
  "inputs": [
    {
      "database": {
        "latest": true
      }
    }
  ],
  "query": $(cat)
}
JSON

Go

go run flare.go
package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io/ioutil"
	"net/http"
)

type QueryRequestBody struct {
	Inputs []interface{} `json:"inputs"`
	Query  string        `json:"query,omitempty"`
	Data   string        `json:"data,omitempty"`
}

type DatabaseInput struct {
	Database struct {
		Latest bool `json:"latest"`
	} `json:"database"`
}

func makeRequest(method string, url string, body QueryRequestBody) string {
	jsonBody, _ := json.Marshal(body)

	client := &http.Client{}
	request, _ := http.NewRequest(method, url, bytes.NewBuffer(jsonBody))
	request.Header.Set("Content-Type", "application/json")

	response, _ := client.Do(request)
	defer response.Body.Close()

	responseBody, _ := ioutil.ReadAll(response.Body)

	return string(responseBody)
}

func main() {
	responseBody := makeRequest("POST", "http://localhost:3042/datomic/transact", QueryRequestBody{
		Data: `
			[{:db/ident       :book/title
			  :db/valueType   :db.type/string
			  :db/cardinality :db.cardinality/one
			  :db/doc         "The title of the book."}

			 {:db/ident       :book/genre
			  :db/valueType   :db.type/string
			  :db/cardinality :db.cardinality/one
			  :db/doc         "The genre of the book."}

			 {:db/ident       :book/published_at_year
			  :db/valueType   :db.type/long
			  :db/cardinality :db.cardinality/one
			  :db/doc         "The year the book was published."}]
		`,
	})

	fmt.Println(responseBody)

	responseBody = makeRequest("POST", "http://localhost:3042/datomic/transact", QueryRequestBody{
		Data: `
			[{:db/id      -1
			  :book/title "The Tell-Tale Heart"
			  :book/genre "Horror"
			  :book/published_at_year 1843}]
		`,
	})

	fmt.Println(responseBody)

	responseBody = makeRequest("GET", "http://localhost:3042/datomic/q", QueryRequestBody{
		Inputs: []interface{}{
			DatabaseInput{Database: struct {
				Latest bool `json:"latest"`
			}{Latest: true}},
			"The Tell-Tale Heart",
		},
		Query: `
			[:find ?e ?title ?genre ?year
			 :in $ ?title
			 :where [?e :book/title ?title]
			        [?e :book/genre ?genre]
			        [?e :book/published_at_year ?year]]
		`,
	})

	fmt.Println(responseBody)
}

JavaScript

You can use standard browser APIs like fetch. However, it does not accept GET requests with a body, so we use POST instead. Read more about GET vs. POST.

async function main() {
  let response;

  response = await fetch('http://localhost:3042/datomic/transact', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      data: `
        [{:db/ident       :book/title
          :db/valueType   :db.type/string
          :db/cardinality :db.cardinality/one
          :db/doc         "The title of the book."}

         {:db/ident       :book/genre
          :db/valueType   :db.type/string
          :db/cardinality :db.cardinality/one
          :db/doc         "The genre of the book."}]
      `
    })
  });

  console.log(await response.json());

  response = await fetch('http://localhost:3042/datomic/transact', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      data: `
        [{:db/id      -1
          :book/title "The Tell-Tale Heart"
          :book/genre "Horror"}]
      `
    })
  });

  console.log(await response.json());

  response = await fetch('http://localhost:3042/datomic/q', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      inputs: [
        { database: { latest: true } }
      ],
      query: `
        [:find ?e ?title ?genre
         :where [?e :book/title ?title]
                [?e :book/genre ?genre]]
      `
    })
  });

  console.log(await response.json());
}

main();

Node.js

Create a package.json file:

{
  "dependencies": {
    "axios": "^1.7.7"
  }
}
npm install

node flare.js
const axios = require('axios');

async function main() {
  let response;

  response = await axios.post('http://localhost:3042/datomic/transact',
    { data: `
        [{:db/ident       :book/title
          :db/valueType   :db.type/string
          :db/cardinality :db.cardinality/one
          :db/doc         "The title of the book."}

         {:db/ident       :book/genre
          :db/valueType   :db.type/string
          :db/cardinality :db.cardinality/one
          :db/doc         "The genre of the book."}]
      `
    },
    { headers: { 'Content-Type': 'application/json' } }
  )


  console.log(response.data);

  response = await axios.post('http://localhost:3042/datomic/transact',
    { data: `
        [{:db/id      -1
          :book/title "The Tell-Tale Heart"
          :book/genre "Horror"}]
      `
    },
    { headers: { 'Content-Type': 'application/json' } }
  );

  console.log(response.data);

  response = await axios.get('http://localhost:3042/datomic/q', {
    headers: { 'Content-Type': 'application/json' },
    data: {
      inputs: [
        { database: { latest: true } }
      ],
      query: `
        [:find ?e ?title ?genre
         :where [?e :book/title ?title]
                [?e :book/genre ?genre]]
      `
    }
  });

  console.log(response.data);
}

main();

PHP

Create a composer.json file:

{
  "require": {
    "guzzlehttp/guzzle": "^7.9.2"
  }
}
composer install

php flare.php
<?php

require 'vendor/autoload.php';

use GuzzleHttp\Client;

$client = new Client([
  'base_uri' => 'http://localhost:3042',
  'headers'  => ['Content-Type' => 'application/json'],
]);

$response = $client->post('/datomic/transact', [
  'json' => [
    'data' => '
      [{:db/ident       :book/title
        :db/valueType   :db.type/string
        :db/cardinality :db.cardinality/one
        :db/doc         "The title of the book."}

       {:db/ident       :book/genre
        :db/valueType   :db.type/string
        :db/cardinality :db.cardinality/one
        :db/doc         "The genre of the book."}]
    '
  ]
]);

echo $response->getBody();

$response = $client->post('/datomic/transact', [
  'json' => [
    'data' => '
      [{:db/id      -1
        :book/title "The Tell-Tale Heart"
        :book/genre "Horror"}]
    '
  ]
]);

echo $response->getBody();

$response = $client->get('/datomic/q', [
  'json' => [
    'inputs' => [
      ['database' => ['latest' => true]]
    ],
    'query' => '
      [:find ?e ?title ?genre
       :where [?e :book/title ?title]
              [?e :book/genre ?genre]]
    '
  ]
]);

echo $response->getBody();

Python

Create a requirements.txt file:

requests ~= 2.32.3
pip install -r requirements.txt

python flare.py
import requests

response = requests.post(
    'http://localhost:3042/datomic/transact',
    headers={'Content-Type': 'application/json'},
    json={
        "data": """
        [{:db/ident       :book/title
          :db/valueType   :db.type/string
          :db/cardinality :db.cardinality/one
          :db/doc         "The title of the book."}

         {:db/ident       :book/genre
          :db/valueType   :db.type/string
          :db/cardinality :db.cardinality/one
          :db/doc         "The genre of the book."}]
        """
    }
)

print(response.json())

response = requests.post(
    'http://localhost:3042/datomic/transact',
    headers={'Content-Type': 'application/json'},
    json={
        "data": """
        [{:db/id      -1
          :book/title "The Tell-Tale Heart"
          :book/genre "Horror"}]
        """
    }
)

print(response.json())

response = requests.get(
    'http://localhost:3042/datomic/q',
    headers={'Content-Type': 'application/json'},
    json={
        "inputs": [
            {"database": {"latest": True}}
        ],
        "query": """
        [:find ?e ?title ?genre
         :where [?e :book/title ?title]
                [?e :book/genre ?genre]]
        """
    }
)

print(response.json())

Ruby

You might want to check out the Flare Ruby Gem.

Create a Gemfile file:

source 'https://rubygems.org'

gem 'faraday', '~> 2.12'
gem 'faraday-typhoeus', '~> 1.1'
gem 'typhoeus', '~> 1.4', '>= 1.4.1'
bundle

bundle exec ruby flare.rb
require 'json'

require 'faraday'
require 'faraday/typhoeus'

Faraday.default_adapter = :typhoeus

response = Faraday.post('http://localhost:3042/datomic/transact') do |request|
  request.headers['Content-Type'] = 'application/json'
  request.body = {
    data: <<~EDN
      [{:db/ident       :book/title
        :db/valueType   :db.type/string
        :db/cardinality :db.cardinality/one
        :db/doc         "The title of the book."}

       {:db/ident       :book/genre
        :db/valueType   :db.type/string
        :db/cardinality :db.cardinality/one
        :db/doc         "The genre of the book."}]
    EDN
  }.to_json
end

puts response.body

response = Faraday.post('http://localhost:3042/datomic/transact') do |request|
  request.headers['Content-Type'] = 'application/json'
  request.body = {
    data: <<~EDN
      [{:db/id      -1
        :book/title "The Tell-Tale Heart"
        :book/genre "Horror"}]
    EDN
  }.to_json
end

puts response.body

response = Faraday.get('http://localhost:3042/datomic/q') do |request|
  request.headers['Content-Type'] = 'application/json'
  request.body = {
    inputs: [
      { database: { latest: true } }
    ],
    query: <<~EDN
      [:find ?e ?title ?genre
       :where [?e :book/title ?title]
              [?e :book/genre ?genre]]
    EDN
  }.to_json
end

puts response.body

Usage

Ensure you have curl, bb, and jq installed.

Meta

curl -s http://localhost:3042/meta \
  -X GET \
  -H "Content-Type: application/json"  \
| jq
{
  "data": {
    "mode": "peer",
    "datomic-flare": "1.0.0",
    "org.clojure/clojure": "1.12.0",
    "com.datomic/peer": "1.0.7187",
    "com.datomic/client-pro": "1.0.81"
  }
}

Creating a Database

In Client Mode, this operation is not supported as the Peer Server does not have it available.

curl -s http://localhost:3042/datomic/create-database \
  -X POST \
  -H "Content-Type: application/json" \
  -d '
{
  "name": "moonlight"
}
' \
| jq
{
  "data": true
}

Deleting a Database

In Client Mode, this operation is not supported as the Peer Server does not have it available.

curl -s http://localhost:3042/datomic/delete-database \
  -X DELETE \
  -H "Content-Type: application/json" \
  -d '
{
  "name": "moonlight"
}
' \
| jq
{
  "data": true
}

Listing Databases

Flare on Peer Mode:

curl -s http://localhost:3042/datomic/get-database-names \
  -X GET \
  -H "Content-Type: application/json"  \
| jq

Flare on Client Mode:

curl -s http://localhost:3042/datomic/list-databases \
  -X GET \
  -H "Content-Type: application/json"  \
| jq
{
  "data": [
    "my-datomic-database"
  ]
}

Transacting Schema

echo '
[{:db/ident       :book/title
  :db/valueType   :db.type/string
  :db/cardinality :db.cardinality/one
  :db/doc         "The title of the book."}

 {:db/ident       :book/genre
  :db/valueType   :db.type/string
  :db/cardinality :db.cardinality/one
  :db/doc         "The genre of the book."}

 {:db/ident       :book/published_at_year
  :db/valueType   :db.type/long
  :db/cardinality :db.cardinality/one
  :db/doc         "The year the book was first published."}]
' \
| bb -e '(pr-str (edn/read-string (slurp *in*)))' \
| curl -s http://localhost:3042/datomic/transact \
  -X POST \
  -H "Content-Type: application/json" \
  --data-binary @- <<JSON \
| jq
{
  "data": $(cat)
}
JSON
{
  "data": {
    "db-before": "datomic.db.Db@6c2743f5",
    "db-after": "datomic.db.Db@7b188823",
    "tx-data": [
      [13194139534315, 50, "2024-10-06T13:23:28.602Z", 13194139534315, true],
      [74, 10, ":book/published_at_year", 13194139534315, true],
      [74, 40, 22, 13194139534315, true],
      [74, 41, 35, 13194139534315, true],
      [74, 62, "The year the book was first published.", 13194139534315, true],
      [0, 13, 74, 13194139534315, true]
    ],
    "tempids": {
      "-9223300668110597889": 72,
      "-9223300668110597888": 73,
      "-9223300668110597887": 74
    }
  }
}

Asserting Facts

echo '
[{:db/id      -1
  :book/title "Pride and Prejudice"
  :book/genre "Romance"
  :book/published_at_year 1813}]
' \
| bb -e '(pr-str (edn/read-string (slurp *in*)))' \
| curl -s http://localhost:3042/datomic/transact \
  -X POST \
  -H "Content-Type: application/json" \
  --data-binary @- <<JSON \
| jq
{
  "data": $(cat)
}
JSON
{
  "data": {
    "db-before": "datomic.db.Db@75d129e2",
    "db-after": "datomic.db.Db@21369e69",
    "tx-data": [
      [13194139534316, 50, "2024-10-06T13:23:28.618Z", 13194139534316, true],
      [4611681620380877805, 72, "Pride and Prejudice", 13194139534316, true],
      [4611681620380877805, 73, "Romance", 13194139534316, true],
      [4611681620380877805, 74, 1813, 13194139534316, true]
    ],
    "tempids": {
      "-1": 4611681620380877805
    }
  }
}
echo '
[{:db/id      -1
  :book/title "Near to the Wild Heart"
  :book/genre "Novel"
  :book/published_at_year 1943}
 {:db/id      -2
  :book/title "A Study in Scarlet"
  :book/genre "Detective"
  :book/published_at_year 1887}
 {:db/id      -3
  :book/title "The Tell-Tale Heart"
  :book/genre "Horror"
  :book/published_at_year 1843}]
' \
| bb -e '(pr-str (edn/read-string (slurp *in*)))' \
| curl -s http://localhost:3042/datomic/transact \
  -X POST \
  -H "Content-Type: application/json" \
  --data-binary @- <<JSON \
| jq
{
  "data": $(cat)
}
JSON
{
  "data": {
    "db-before": "datomic.db.Db@55451c2f",
    "db-after": "datomic.db.Db@6a23cf34",
    "tx-data": [
      [13194139534318, 50, "2024-10-06T13:23:28.636Z", 13194139534318, true],
      [4611681620380877807, 72, "Near to the Wild Heart", 13194139534318, true],
      [4611681620380877807, 73, "Novel", 13194139534318, true],
      [4611681620380877807, 74, 1943, 13194139534318, true],
      [4611681620380877808, 72, "A Study in Scarlet", 13194139534318, true],
      [4611681620380877808, 73, "Detective", 13194139534318, true],
      [4611681620380877808, 74, 1887, 13194139534318, true],
      [4611681620380877809, 72, "The Tell-Tale Heart", 13194139534318, true],
      [4611681620380877809, 73, "Horror", 13194139534318, true],
      [4611681620380877809, 74, 1843, 13194139534318, true]
    ],
    "tempids": {
      "-1": 4611681620380877807,
      "-2": 4611681620380877808,
      "-3": 4611681620380877809
    }
  }
}

Reading Data by Entity

In Client Mode, this operation is not supported as the Peer Server does not have it available.

curl -s http://localhost:3042/datomic/entity \
  -X GET \
  -H "Content-Type: application/json" \
  -d '
{
  "database": {
    "latest": true
  },
  "id": 4611681620380877807
}
' \
| jq
{
  "data": {
    ":book/title": "Near to the Wild Heart",
    ":book/genre": "Novel",
    ":book/published_at_year": 1943,
    ":db/id": 4611681620380877807
  }
}

Reading Data by Querying

echo '
[:find ?e ?title ?genre ?year
 :where [?e :book/title ?title]
        [?e :book/genre ?genre]
        [?e :book/published_at_year ?year]]
' \
| bb -e '(pr-str (edn/read-string (slurp *in*)))' \
| curl -s http://localhost:3042/datomic/q \
  -X GET \
  -H "Content-Type: application/json" \
  --data-binary @- <<JSON \
| jq
{
  "inputs": [
    {
      "database": {
        "latest": true
      }
    }
  ],
  "query": $(cat)
}
JSON
{
  "data": [
    [
      4611681620380877808,
      "A Study in Scarlet",
      "Detective",
      1887
    ],
    [
      4611681620380877807,
      "Near to the Wild Heart",
      "Novel",
      1943
    ],
    [
      4611681620380877809,
      "The Tell-Tale Heart",
      "Horror",
      1843
    ],
    [
      4611681620380877805,
      "Pride and Prejudice",
      "Romance",
      1813
    ]
  ]
}
echo '
[:find ?e ?title ?genre ?year
 :in $ ?title
 :where [?e :book/title ?title]
        [?e :book/genre ?genre]
        [?e :book/published_at_year ?year]]
' \
| bb -e '(pr-str (edn/read-string (slurp *in*)))' \
| curl -s http://localhost:3042/datomic/q \
  -X GET \
  -H "Content-Type: application/json" \
  --data-binary @- <<JSON \
| jq
{
  "inputs": [
    {
      "database": {
        "latest": true
      }
    },
    "The Tell-Tale Heart"
  ],
  "query": $(cat)
}
JSON
{
  "data": [
    [
      4611681620380877809,
      "The Tell-Tale Heart",
      "Horror",
      1843
    ]
  ]
}

Accumulating Facts

echo '
[{:db/id 4611681620380877806 :book/genre "Gothic"}]
' \
| bb -e '(pr-str (edn/read-string (slurp *in*)))' \
| curl -s http://localhost:3042/datomic/transact \
  -X POST \
  -H "Content-Type: application/json" \
  --data-binary @- <<JSON \
| jq
{
  "data": $(cat)
}
JSON
{
  "data": {
    "db-before": "datomic.db.Db@164175ab",
    "db-after": "datomic.db.Db@31164dc",
    "tx-data": [
      [13194139534322, 50, "2024-10-06T13:23:28.701Z", 13194139534322, true],
      [4611681620380877806, 73, "Gothic", 13194139534322, true]
    ],
    "tempids": {
    }
  }
}

Retracting Facts

Retract the value of an attribute:

echo '
[[:db/retract 4611681620380877806 :book/genre "Gothic"]]
' \
| bb -e '(pr-str (edn/read-string (slurp *in*)))' \
| curl -s http://localhost:3042/datomic/transact \
  -X POST \
  -H "Content-Type: application/json" \
  --data-binary @- <<JSON \
| jq
{
  "data": $(cat)
}
JSON
{
  "data": {
    "db-before": "datomic.db.Db@7e48b4f5",
    "db-after": "datomic.db.Db@ae68e67",
    "tx-data": [
      [13194139534323, 50, "2024-10-06T13:23:28.715Z", 13194139534323, true],
      [4611681620380877806, 73, "Gothic", 13194139534323, false]
    ],
    "tempids": {
    }
  }
}

Retract an attribute:

echo '
[[:db/retract 4611681620380877804 :book/genre]]
' \
| bb -e '(pr-str (edn/read-string (slurp *in*)))' \
| curl -s http://localhost:3042/datomic/transact \
  -X POST \
  -H "Content-Type: application/json" \
  --data-binary @- <<JSON \
| jq
{
  "data": $(cat)
}
JSON
{
  "data": {
    "db-before": "datomic.db.Db@17175ebd",
    "db-after": "datomic.db.Db@1f28ff07",
    "tx-data": [
      [13194139534324, 50, "2024-10-06T13:23:28.729Z", 13194139534324, true]
    ],
    "tempids": {
    }
  }
}

Retract an entity:

echo '
[[:db/retractEntity 4611681620380877805]]
' \
| bb -e '(pr-str (edn/read-string (slurp *in*)))' \
| curl -s http://localhost:3042/datomic/transact \
  -X POST \
  -H "Content-Type: application/json" \
  --data-binary @- <<JSON \
| jq
{
  "data": $(cat)
}
JSON
{
  "data": {
    "db-before": "datomic.db.Db@24fc1f0b",
    "db-after": "datomic.db.Db@1ad72845",
    "tx-data": [
      [13194139534325, 50, "2024-10-06T13:23:28.745Z", 13194139534325, true],
      [4611681620380877805, 72, "Pride and Prejudice", 13194139534325, false],
      [4611681620380877805, 73, "Romance", 13194139534325, false],
      [4611681620380877805, 74, 1813, 13194139534325, false]
    ],
    "tempids": {
    }
  }
}

Retrieving Datoms from an Index

curl -s http://localhost:3042/datomic/datoms \
  -X GET \
  -H "Content-Type: application/json" \
  -d '
{
  "database": {
    "latest": true
  },
  "index": "eavt"
}
' \
| jq
{
  "data": [
    [0, 10, "db", 0, true],
    [0, 11, 0, 54, true],
    [0, 11, 3, 0, true],
    [0, 11, 4, 0, true]
  ]
}

Architecture

Here are samples of how you may deploy Flare in your infrastructure.

Embedded Peers

graph RL
    Transactor --- Storage[(Storage)]
    Flare("Flare (Peer)") -.- Storage
    Flare --- Transactor
    Application([Application]) --- Flare
Loading
graph RL
    Transactor --- Storage[(Storage)]
    FlareA("Flare (Peer)") -.- Storage
    FlareB("Flare (Peer)") -.- Storage
    FlareA --- Transactor
    FlareB --- Transactor
    ApplicationA([Application]) --- FlareA
    ApplicationB([Application]) --- FlareA
    ApplicationC([Application]) --- FlareB
    ApplicationD([Application]) --- FlareB
Loading

Peer Servers

graph RL
    Transactor --- Storage[(Storage)]
    PeerServer --- Transactor
    PeerServer -.- Storage
    Flare(Flare) --- PeerServer[Peer Server]
    Application([Application]) --- Flare
Loading
graph RL
    Transactor --- Storage[(Storage)]
    PeerServer --- Transactor
    PeerServer -.- Storage
    FlareA(Flare) --- PeerServer[Peer Server]
    FlareB(Flare) --- PeerServer
    ApplicationA([Application]) --- FlareA
    ApplicationB([Application]) --- FlareA
    ApplicationC([Application]) --- FlareB
    ApplicationD([Application]) --- FlareB
Loading
graph RL
    Transactor --- Storage[(Storage)]
    PeerServerA[Peer Server] --- Transactor
    PeerServerA -.- Storage
    PeerServerB[Peer Server] --- Transactor
    PeerServerB -.- Storage
    FlareA(Flare) --- PeerServerA
    FlareB(Flare) --- PeerServerA
    FlareC(Flare) --- PeerServerB
    FlareD(Flare) --- PeerServerB
    ApplicationA([Application]) --- FlareA
    ApplicationB([Application]) --- FlareA
    ApplicationC([Application]) --- FlareB
    ApplicationD([Application]) --- FlareB
    ApplicationE([Application]) --- FlareC
    ApplicationF([Application]) --- FlareC
    ApplicationG([Application]) --- FlareD
    ApplicationH([Application]) --- FlareD
Loading

About

Characteristics

  • Languages that play well with HTTP and JSON can interact with Datomic right away;

  • Plug and play into any of the many flavors of Datomic's flexible infrastructure architecture;

  • Minimal and transparent layer, not a DSL or framework, just straightforward access to Datomic;

  • Despite JSON, both queries and transactions are done in edn, enabling, e.g., powerful Datalog queries.

Trade-offs

  • Languages have different data types, so edn -> JSON -> [Your Language] and vice-versa: something will be lost in translation and expressiveness;

  • An extra layer in the architecture adds a new hop to requests, potentially increasing latency;

  • Being one step away from Clojure reduces our power to fully leverage its types, data structures, immutability, and other desired properties;

  • Some tricks that would be easy to do in Clojure + Datomic become more cumbersome: transaction functions, advanced Datalog datasources, lazy loading, etc.

GET vs. POST

This API offers GET endpoints that accept a JSON body, an approach used by projects like Elasticsearch.

If you encounter issues, switch from GET to POST. The server will mirror the behavior of the GET requests.

Semantically, GET should not have bodies. Semantically, POST implies side effects, which is not the case here. So, we are aware that both are not ideal.

We considered using query strings with GET, but parsing was cumbersome and length limits were a risk, so we use GET with a body and POST as a fallback.

Future HTTP evolutions may address this use case, such as the proposed HTTP QUERY method.

Development

clj -M:run
clj -M:repl
clj -M:format
clj -M:lint
cljfmt fix deps.edn src/
clj-kondo --lint deps.edn src/