├── .gitignore ├── package.json ├── src └── datomic_cljs │ ├── rest_client.cljs │ ├── util.cljs │ ├── macros.clj │ ├── http.cljs │ └── api.cljs ├── index.html ├── resources ├── friend_data.edn └── friend_schema.edn ├── test └── datomic_cljs │ ├── test_macros.clj │ └── t_api.cljs ├── LICENSE ├── project.clj ├── examples └── friends.cljs └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | .lein* 3 | .repl 4 | out -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies" : { 3 | "request" : "2.27.0" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/datomic_cljs/rest_client.cljs: -------------------------------------------------------------------------------- 1 | (ns datomic-cljs.rest-client 2 | (:require [datomic-cljs.http :as http])) 3 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/datomic_cljs/util.cljs: -------------------------------------------------------------------------------- 1 | (ns datomic-cljs.util 2 | (:require [cljs.core.async :as async])) 3 | 4 | (defn singleton-chan 5 | "Returns a closed core.async channel containing only element." 6 | [element] 7 | (let [c (async/chan 1)] 8 | (async/put! c element #(async/close! c)) 9 | c)) 10 | -------------------------------------------------------------------------------- /resources/friend_data.edn: -------------------------------------------------------------------------------- 1 | [ 2 | 3 | {:db/id #db/id[:db.part/user -1] 4 | :person/name "Frank" 5 | :person/age 111} 6 | 7 | {:db/id #db/id[:db.part/user -2] 8 | :person/name "Becky" 9 | :person/age 222 10 | :person/friends #{#db/id[:db.part/user -1]}} 11 | 12 | {:db/id #db/id[:db.part/user -3] 13 | :person/name "Caroll" 14 | :person/age 333 15 | :person/friends #{#db/id[:db.part/user -1] 16 | #db/id[:db.part/user -2]}} 17 | 18 | ] 19 | -------------------------------------------------------------------------------- /src/datomic_cljs/macros.clj: -------------------------------------------------------------------------------- 1 | (ns datomic-cljs.macros) 2 | 3 | (defmacro >!x 4 | "The same as (do (>! c val) (close! c))" 5 | [c val] 6 | `(let [c# ~c] 7 | (cljs.core.async/>! c# ~val) 8 | (cljs.core.async/close! c#))) 9 | 10 | (defmacro 11 | "Takes a value from a core.async channel, throwing the value if it 12 | is a js/Error." 13 | [c] 14 | `(let [val# (cljs.core.async/!]] 4 | [cljs.nodejs :as nodejs]) 5 | (:require-macros [cljs.core.async.macros :refer [go]] 6 | [datomic-cljs.macros :refer []])) 7 | 8 | (def js-fs (nodejs/require "fs")) 9 | 10 | (def friend-schema (.readFileSync js-fs "resources/friend_schema.edn" "utf8")) 11 | (def friend-data (.readFileSync js-fs "resources/friend_data.edn" "utf8")) 12 | 13 | (defn -main [& args] 14 | (go 15 | 16 | ;; Create and connect to a new Datomic database. 17 | (let [conn ( (d/create-database "localhost" 9898 "db" "friends"))] 18 | 19 | ;; Transact our schema and seed data. 20 | ( (d/transact conn friend-schema)) 21 | ( (d/transact conn friend-data)) 22 | 23 | ;; Find Caroll's entity id. 24 | (let [caroll-eid (ffirst ( (d/q '[:find ?e :where [?e :person/name "Caroll"]] 25 | (d/db conn))))] 26 | 27 | ;; Using her entity id, get her entity map, and use that to 28 | ;; see all her friends. 29 | (println (-> ( (d/entity (d/db conn) caroll-eid)) 30 | :person/friends))) 31 | 32 | ;; Frank wants to start going by Franky. 33 | (let [frank-eid (ffirst ( (d/q '[:find ?e :where [?e :person/name "Frank"]] 34 | (d/db conn)))) 35 | 36 | tx-data ( (d/transact conn [[:db/add frank-eid :person/name "Franky"]])) 37 | 38 | {{before-t :basis-t} :db-before} tx-data] 39 | 40 | ;; But we know he used to just be Frank. 41 | (println ( (d/q '[:find ?n :in $ ?e :where [?e :person/name ?n]] 42 | (d/as-of (d/db conn) before-t) 43 | frank-eid))))))) 44 | 45 | (set! *main-cli-fn* -main) 46 | -------------------------------------------------------------------------------- /src/datomic_cljs/http.cljs: -------------------------------------------------------------------------------- 1 | (ns datomic-cljs.http 2 | (:require [cljs.core.async :as async :refer [!x]])) 7 | 8 | (def node-context? 9 | (and (exists? js/exports) 10 | (not= js/exports (this-as context (.-exports context))))) 11 | 12 | ;;; browser shims 13 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 14 | 15 | (defn- urlencode-kvs [kvs] 16 | (->> (for [[k v] kvs 17 | :when (not (nil? v))] 18 | (str (js/encodeURIComponent (name k)) 19 | "=" 20 | (js/encodeURIComponent v))) 21 | (str/join "&"))) 22 | 23 | (defn- urlencode-qs [qs-kvs] 24 | (str "?" (urlencode-kvs qs-kvs))) 25 | 26 | (defn- parse-headers [header-str] 27 | (into {} (for [header (str/split-lines header-str) 28 | :let [[k v] (str/split header #":") 29 | [k v] [(str/trim k) (str/trim v)]]] 30 | [(keyword (str/lower-case k)) v]))) 31 | 32 | (defn- browser-request [{:keys [method uri headers qs form] 33 | :or {method "GET" headers {}} 34 | :as opts} 35 | callback] 36 | (let [js-req (js/XMLHttpRequest.) 37 | query-str (if qs (urlencode-qs qs) "") 38 | url (str uri query-str) 39 | headers (if form 40 | (assoc headers "Content-Type" "application/x-www-form-urlencoded") 41 | headers)] 42 | 43 | (.addEventListener js-req "load" 44 | (fn [] 45 | ;; emulate node response... sort of 46 | (set! (.-headers js-req) 47 | (parse-headers (.getAllResponseHeaders js-req))) 48 | (set! (.-statusCode js-req) (.-status js-req)) 49 | (callback nil js-req (.-response js-req)))) 50 | 51 | ;; The REST server 'sploding probably results in a CORS error on our end 52 | (.addEventListener js-req "error" 53 | (fn [e] 54 | (.preventDefault e) 55 | (callback e js-req nil))) 56 | 57 | (.open js-req method url true) 58 | 59 | (doseq [[k v] (or headers {})] 60 | (.setRequestHeader js-req (name k) v)) 61 | 62 | (set! (.-responseType js-req) "text") 63 | 64 | (if form 65 | (.send js-req (urlencode-kvs form)) 66 | (.send js-req)))) 67 | 68 | 69 | (def ^:private js-request nil) 70 | (if node-context? 71 | (set! js-request (let [req (try (js/require "request") 72 | (catch js/Error e 73 | (.log js/console "Error: Cannot find module 'request'.\nSee datomic-cljs README for installation and dependency notes.") 74 | (.exit js/process 1)))] 75 | (fn [opts cb] 76 | (req (clj->js opts) cb)))) 77 | (set! js-request browser-request)) 78 | 79 | (defn- response-handler [c-resp edn?] 80 | (fn [err resp body] 81 | (async/put! 82 | c-resp 83 | (or err 84 | (let [headers (js->clj (.-headers resp) 85 | :keywordize-keys true)] 86 | {:status (.-statusCode resp) 87 | :headers headers 88 | :body (if (and edn? (re-find #"edn" (:content-type headers))) 89 | (reader/read-string body) 90 | body) 91 | :js-resp resp})) 92 | #(async/close! c-resp)))) 93 | 94 | (defn request 95 | "Make an async request to the given uri, returning a core.async 96 | channel eventually containing either an error or a response map 97 | containing the following: 98 | 99 | :status, the HTTP status code; 100 | :headers, a map of HTTP response headers; 101 | :body, the response body; 102 | :js-resp, the original JS response object. 103 | 104 | opts is the same options map described in the Request docs: 105 | https://github.com/mikeal/request#requestoptions-callback 106 | 107 | Additionally, opts supports {:edn true} which sets the Accept 108 | header to application/edn and parses the response body as edn 109 | if the response content-type is application/edn." 110 | ([method uri] 111 | (request method uri {})) 112 | ([method uri opts] 113 | (let [c-resp (async/chan 1) 114 | {edn? :edn headers :headers} opts 115 | opts (assoc opts 116 | :method (case method 117 | :get "GET" 118 | :post "POST" 119 | :put "PUT" 120 | :head "HEAD") 121 | :headers (if edn? 122 | (assoc (or headers {}) :accept "application/edn") 123 | headers))] 124 | (js-request (assoc opts :uri uri) 125 | (response-handler c-resp edn?)) 126 | c-resp))) 127 | 128 | (defn body 129 | "Expects a response channel, and returns a channel that will 130 | eventually contain either the response body (on successful status 131 | code) or an error (if the request fails or an unsuccessful status 132 | code was returned)." 133 | [c-resp] 134 | (let [c-body (async/chan 1)] 135 | (go 136 | (let [resp (!x c-body 138 | (cond (instance? js/Error resp) 139 | resp 140 | 141 | (not (<= 200 (:status resp) 299)) 142 | (js/Error. (str "Unsuccessful HTTP status code returned: " (:status resp))) 143 | 144 | :else 145 | (:body resp))))) 146 | c-body)) 147 | -------------------------------------------------------------------------------- /test/datomic_cljs/t_api.cljs: -------------------------------------------------------------------------------- 1 | (ns datomic-cljs.t-api 2 | (:refer-clojure :exclude [test]) 3 | (:require [datomic-cljs.api :as d] 4 | [datomic-cljs.http :as http] 5 | [cljs.reader :as reader] 6 | [cljs.core.async :as async :refer [!]]) 7 | (:require-macros [cljs.core.async.macros :refer [go]] 8 | [datomic-cljs.macros :refer []] 9 | [datomic-cljs.test-macros :refer [go-test-all test]])) 10 | 11 | ;; ASSUMPTIONS: 12 | ;; 1. you have a Datomic REST service running on localhost:9898 13 | ;; 2. it has a transactor alias called 'db' 14 | ;; 3. you don't care that a bunch of random test dbs are going 15 | ;; to be created; we can't delete them yet from the REST api 16 | ;; 4. if you're running this in the browser, you've set up CORS 17 | ;; permissions; see "Using Datomic REST" in the README 18 | 19 | (def test-db-name (str "datomic-cljs-test-" (rand-int 1e8))) 20 | (.log js/console "Starting tests using db" test-db-name) 21 | 22 | (def connect-args ["localhost" 9898 "db" test-db-name]) 23 | 24 | (defn all-the-tests [schema data] 25 | (go-test-all 26 | 27 | (test "can read and print db/id tagged literals" 28 | (try 29 | (reader/read-string "#db/id :foo") 30 | false 31 | (catch js/Error e 32 | true)) 33 | (instance? d/DbId (reader/read-string "#db/id[:db.part/db]")) 34 | (= "#db/id[:db.part/db]" (str (reader/read-string "#db/id[:db.part/db]"))) 35 | (= "#db/id[:db.part/db -1]" (str (reader/read-string "#db/id[:db.part/db -1]")))) 36 | 37 | (test "can create a new database" 38 | (let [conn ( (apply d/create-database connect-args))] 39 | (satisfies? d/IDatomicConnection conn))) 40 | 41 | (test "can connect to an existing database" 42 | (satisfies? d/IDatomicConnection (apply d/connect connect-args))) 43 | 44 | (let [conn (apply d/connect connect-args) 45 | schema-tx-data ( (d/transact conn schema)) 46 | data-tx-data ( (d/transact conn data)) 47 | 48 | ;; helper queries 49 | all-names '[:find ?n :where [?_ :person/name ?n]]] 50 | 51 | (test "can transact schema and data" 52 | (and (map? schema-tx-data) 53 | (map? data-tx-data))) 54 | 55 | (test "successful transactions include actual database values in their result" 56 | (satisfies? d/IDatomicDB (:db-before schema-tx-data)) 57 | (satisfies? d/IDatomicDB (:db-after schema-tx-data))) 58 | 59 | (test "can make simple queries" 60 | (let [result ( (d/q all-names (d/db conn)))] 61 | (= 3 (count result)))) 62 | 63 | (test "can query with inputs" 64 | (-> ( (d/q '[:find ?e :in $ ?n :where [?e :person/name ?n]] 65 | (d/db conn) "Frank")) 66 | ffirst 67 | number?)) 68 | 69 | (test "can submit a malformed query without shit totally blowing up" 70 | (instance? js/Error ( (d/db conn) 74 | (d/limit 1)) 75 | result ( (d/q all-names db))] 76 | (= 1 (count result)))) 77 | 78 | (test "can offset query results" 79 | (let [db (-> (d/db conn) 80 | (d/offset 1)) 81 | result ( (d/q all-names db))] 82 | (= 2 (count result)))) 83 | 84 | (test "can compose limit/offset" 85 | (let [db (-> (d/db conn) 86 | (d/offset 1) 87 | (d/limit 1)) 88 | result ( (d/q all-names db))] 89 | (= 1 (count result)))) 90 | 91 | (test "can query with inputs" 92 | (-> ( (d/q '[:find ?e :in $ ?n :where [?e :person/name ?n]] 93 | (d/db conn) "Frank")) 94 | ffirst 95 | number?)) 96 | 97 | (test "can access entity maps" 98 | (let [query '[:find ?e :where [?e :person/name "Caroll"]] 99 | eid (ffirst ( (d/q query (d/db conn))))] 100 | (->> ( (d/entity (d/db conn) eid)) 101 | :person/friends 102 | count 103 | (= 2)))) 104 | 105 | (test "can query past database value" 106 | (let [eid-query '[:find ?e :where [?e :person/name "Becky"]] 107 | [[eid]] ( (d/q eid-query (d/db conn))) 108 | 109 | tx-data [[:db/add eid :person/name "Wilma"]] 110 | {db-before :db-before} ( (d/transact conn tx-data)) 111 | 112 | name-query '[:find ?n :in $ ?e :where [?e :person/name ?n]] 113 | before (ffirst ( (d/q name-query db-before eid))) 114 | after (ffirst ( (d/q name-query (d/db conn) eid)))] 115 | (and (= before "Becky") 116 | (= after "Wilma")))) 117 | 118 | (test "can access the basis-t of a db" 119 | (let [t ( (d/basis-t (d/db conn)))] 120 | (and (number? t) 121 | (= (dec t) ( (d/basis-t (d/as-of (d/db conn) (dec t)))))))) 122 | 123 | (test "can get entity ids from idents and vice versa" 124 | (let [eid ( (d/entid (d/db conn) :person/age))] 125 | (and (number? eid) 126 | (= :person/age ( (d/ident (d/db conn) eid))))) 127 | (= 12345 ( (d/entid (d/db conn) 12345))) 128 | (= :person/age ( (d/ident (d/db conn) :person/age)))) 129 | 130 | (test "can access raw index data with datoms" 131 | (-> ( (d/datoms (d/limit (d/db conn) 10) :eavt)) 132 | first :e (= 0))) 133 | 134 | (test "can narrow raw index data result by specifying components" 135 | (let [data ( (d/datoms (d/db conn) :eavt :e 0))] 136 | (every? #(= 0 (:e %)) data))) 137 | 138 | (test "squuid (probably) conforms to RFC4122" 139 | (let [validator (js/RegExp. "^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" "i")] 140 | (every? identity 141 | (map (fn [_] 142 | (re-matches validator (.-uuid (d/squuid)))) 143 | (range 100))))) 144 | 145 | (test "can retrieve millis component of a squuid" 146 | (let [seconds (Math/round (/ (.now js/Date) 1000)) 147 | time-ms (d/squuid-time-millis (d/squuid))] 148 | (= seconds (/ time-ms 1000)))) 149 | 150 | (test "can generate unique tempids" 151 | (let [ids (repeatedly 3 #(d/tempid :db.part/user)) 152 | specs (map #(.-spec %) ids) 153 | ns (map second specs)] 154 | (and (apply not= specs) 155 | (every? #(< % -1000000) ns)))) 156 | 157 | (comment 158 | (test "history") 159 | (test "index-range"))))) 160 | 161 | (if http/node-context? 162 | (let [js-fs (js/require "fs")] 163 | (set! *main-cli-fn* (fn [] 164 | (all-the-tests (.readFileSync js-fs "resources/friend_schema.edn" "utf8") 165 | (.readFileSync js-fs "resources/friend_data.edn" "utf8"))))) 166 | (go 167 | (let [schema (> ( (d/entity db eid)) 145 | :person/friends 146 | (map #(d/entity db %)))] 147 | ( (first friends)))) 148 | ``` 149 | 150 | ##### `datomic-cljs.api/limit` 151 | 152 | Added. 153 | This limits the results to a certain number. 154 | It composes in the same way as the other query operations. 155 | 156 | ##### `datomic-cljs.api/offset` 157 | 158 | Added. 159 | This begins returning results at a certain number. 160 | It composes in the same way as the other query operations. 161 | 162 | ### Error Handling 163 | 164 | Errors are put directly on return channels. 165 | To keep from wrapping each parking take in a conditional, `datomic-cljs.macros/` is introduced. 166 | Instead of `cljs.core.async/! !x]])) 8 | 9 | 10 | ;;; Tagged literals 11 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 12 | 13 | (deftype DbId [spec] 14 | Object 15 | (toString [_] 16 | (str "#db/id" spec)) 17 | 18 | IPrintWithWriter 19 | (-pr-writer [this writer _] 20 | (-write writer (str this)))) 21 | 22 | (defn- read-dbid 23 | [spec] 24 | (if (vector? spec) 25 | (DbId. spec) 26 | (reader/reader-error nil "db/id literal expects a vector as its representation."))) 27 | 28 | (reader/register-tag-parser! "db/id" read-dbid) 29 | 30 | (let [!next-id (atom -1000001)] 31 | (defn tempid 32 | "Generate a tempid in the specified partition. Values of n from -1 33 | to -1000000, inclusive, are reserved for user-created tempids." 34 | ([partition] 35 | (let [id (DbId. [partition @!next-id])] 36 | (swap! !next-id dec) 37 | id)) 38 | ([partition n] 39 | (DbId. [partition n])))) 40 | 41 | ;;; Protocols/implementations 42 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 43 | 44 | (defprotocol IDatomicDB 45 | (-q [db query inputs]) 46 | (-entity [db eid]) 47 | (-datoms [db index components]) 48 | (-basis-t [db])) 49 | 50 | (defprotocol IDatomicConnection 51 | (-conn-url-root [conn]) 52 | (-transact [conn tx-data-str])) 53 | 54 | (defrecord DatomicConnection [hostname port db-alias] 55 | IDatomicConnection 56 | (-conn-url-root [_] 57 | (str "http://" hostname ":" port)) 58 | 59 | (-transact [conn tx-data-str] 60 | ;; TODO: return database values as :db-before and :db-after 61 | (let [path (str (-conn-url-root conn) "/data/" db-alias "/")] 62 | (http/body 63 | (http/request :post path {:edn true 64 | :form {:tx-data tx-data-str}}))))) 65 | 66 | (defrecord DatomicDB [conn implicit-args implicit-qs] 67 | IDatomicDB 68 | (-q [_ query inputs] 69 | (let [args (vec (cons implicit-args inputs)) 70 | path (str (-conn-url-root conn) "/api/query")] 71 | (http/body 72 | (http/request :get path {:edn true 73 | :qs (assoc implicit-qs 74 | :q (prn-str query) 75 | :args (prn-str args))})))) 76 | 77 | (-datoms [_ index components] 78 | (let [path (str (-conn-url-root conn) "/data/" (:db/alias implicit-args) "/-/datoms")] 79 | (http/body 80 | (http/request :get path {:edn true 81 | :qs (assoc (merge implicit-qs components) 82 | :index (name index))})))) 83 | 84 | (-entity [_ eid] 85 | (let [path (str (-conn-url-root conn) "/data/" (:db/alias implicit-args) "/-/entity")] 86 | (http/body 87 | (http/request :get path {:edn true 88 | :qs {:e eid 89 | :as-of (:as-of implicit-args) 90 | :since (:since implicit-args)}})))) 91 | 92 | (-basis-t [_] 93 | (let [c-basis (async/chan 1)] 94 | (go 95 | (if (:as-of implicit-args) 96 | (>!x c-basis (:as-of implicit-args)) 97 | (let [path (str (-conn-url-root conn) 98 | "/data/" (:db-alias conn) 99 | "/" (or (:as-of implicit-args) "-") "/") 100 | res (!x c-basis res) 103 | (>!x c-basis (-> res :body :basis-t)))))) 104 | c-basis))) 105 | 106 | 107 | ;;; Mimicking datomic.api 108 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 109 | 110 | (defn connect 111 | "Create an abstract connection to a Datomic REST service by passing 112 | the following arguments: 113 | 114 | hostname, e.g. localhost; 115 | port, the port on which the REST service is listening; 116 | alias, the transactor alias; 117 | dbname, the name of the database being connected to." 118 | [hostname port alias db-name] 119 | (->DatomicConnection hostname port (str alias "/" db-name))) 120 | 121 | (defn create-database 122 | "Create or connect to a Datomic database via a Datomic REST service 123 | by passing the following arguments: 124 | 125 | hostname, e.g. localhost; 126 | port, the port on which the REST service is listening; 127 | alias, the transactor alias; 128 | db-name, the name of the database being created. 129 | 130 | Returns a core.async channel eventually containing a database 131 | connection (as if using datomic-cljs.api/connect), or an error." 132 | [hostname port alias db-name] 133 | (let [c-conn (async/chan 1) 134 | conn (connect hostname port alias db-name)] 135 | (go 136 | (let [path (str (-conn-url-root conn) "/data/" alias "/") 137 | {:keys [status] :as res} (!x c-conn res) 141 | (or (= status 200) (= status 201)) 142 | (>!x c-conn conn) 143 | :else 144 | (>!x c-conn (js/Error. 145 | (str "Could not create or connect to db: " status)))))) 146 | c-conn)) 147 | 148 | (defn db 149 | "Creates an abstract Datomic value that can be queried." 150 | [{:keys [db-alias] :as conn}] 151 | (->DatomicDB conn {:db/alias db-alias} {})) 152 | 153 | (defn as-of 154 | "Returns the value of the database as of some point t, inclusive. 155 | t can be a transaction number, transaction ID, or inst." 156 | [db t] 157 | (update-in db [:implicit-args] assoc :as-of t)) 158 | 159 | (defn since 160 | "Returns the value of the database since some point t, exclusive. 161 | t can be a transaction number, transaction ID, or inst." 162 | [db t] 163 | (update-in db [:implicit-args] assoc :since t)) 164 | 165 | (defn history 166 | "Returns a special database value containing all assertions and 167 | retractions across time. This database value can be used with 168 | datoms and index-range calls." 169 | [db] 170 | (update-in db [:implicit-qs] assoc :history true)) 171 | 172 | (defn limit 173 | "Returns a value of the database that limits the number of results 174 | from query and datoms to given number n." 175 | [db n] 176 | (update-in db [:implicit-qs] assoc :limit n)) 177 | 178 | (defn offset 179 | "Returns a value of the database that offsets the results of query 180 | and datoms by given number n." 181 | [db n] 182 | (update-in db [:implicit-qs] assoc :offset n)) 183 | 184 | (defn as-of-t 185 | "Returns the as-of point, or nil if none." 186 | [{{as-of :as-of} :implicit-args}] 187 | as-of) 188 | 189 | (defn since-t 190 | "Returns the since point, or nil if none." 191 | [{{since :since} :implicit-args}] 192 | since) 193 | 194 | (defn basis-t 195 | "Returns a core.async channel eventually containing the t of the 196 | the most recent transaction available via this db value." 197 | [db] 198 | (-basis-t db)) 199 | 200 | (defn transact 201 | "Submits a transaction to the database for writing. The transaction 202 | data is sent to the Transactor and, if transactAsync, processed 203 | asynchronously. 204 | 205 | tx-data is a list of lists, each of which specifies a write 206 | operation, either an assertion, a retraction or the invocation of 207 | a data function. Each nested list starts with a keyword identifying 208 | the operation followed by the arguments for the operation. 209 | 210 | Returns a core.async channel that will contain a map with the 211 | following keys: 212 | 213 | :db-before, the database value before the transaction; 214 | :db-after, the database value after the transaction; 215 | :tx-data, the collection of Datums produced by the transaction; 216 | :tempids, an argument to resolve-tempids." 217 | [conn tx-data] 218 | (let [f (fn [body] 219 | (if (map? body) 220 | (assoc body 221 | :db-before (as-of (db conn) (get-in body [:db-before :basis-t])) 222 | :db-after (as-of (db conn) (get-in body [:db-after :basis-t]))) 223 | body))] 224 | (async/map f 225 | [(-transact conn (if (string? tx-data) tx-data (prn-str tx-data)))] 226 | 1))) 227 | 228 | (defn q 229 | "Execute a query against a database value with inputs. Returns a 230 | core.async channel that will contain the result of the query, and 231 | will be closed when the query is complete." 232 | [query db & inputs] 233 | (-q db query inputs)) 234 | 235 | (defn- q-ffirst 236 | [query db & inputs] 237 | (let [c-res (async/chan 1)] 238 | (go 239 | (let [res (!x c-res res) 242 | (>!x c-res (ffirst res))))) 243 | c-res)) 244 | 245 | (defn entity 246 | "Returns a map of the entity's attributes for the given id." 247 | [db eid] 248 | (-entity db eid)) 249 | 250 | (defn entid 251 | "Returns a core.async channel that will contain the entity id 252 | associated with a symbolic keyword, or the id itself if passed." 253 | [db ident] 254 | (if (number? ident) 255 | (util/singleton-chan ident) 256 | (q-ffirst '[:find ?e :in $ ?ident :where [?e :db/ident ?ident]] db ident))) 257 | 258 | (defn ident 259 | "Returns a core.async channel that will contain the ident 260 | associated with an entity id, or the ident itself if passed." 261 | [db eid] 262 | (if (keyword? eid) 263 | (util/singleton-chan eid) 264 | (q-ffirst '[:find ?ident :in $ ?e :where [?e :db/ident ?ident]] db eid))) 265 | 266 | (defn datoms 267 | "Raw access to the index data, by index. The index must be 268 | supplied, along with optional leading components." 269 | [db index & {:as components}] 270 | (-datoms db index components)) 271 | 272 | (defn index-range 273 | "Returns a range of datoms in the given index, starting from start, 274 | or the beginning if start is nil, and going to end, or through the 275 | end if end is nil." 276 | [db index start end] 277 | (-datoms db index {:start start :end end})) 278 | 279 | (defn- squuid-seconds-component 280 | "Returns the current time rounded to the nearest second." 281 | [] 282 | (-> (.now js/Date) 283 | (/ 1000) 284 | (Math/round))) 285 | 286 | ;; http://stackoverflow.com/a/2117523 287 | ;; 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { 288 | ;; var r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8); 289 | ;; return v.toString(16); 290 | ;; }); 291 | (defn squuid 292 | "Constructs a semi-sequential UUID. Useful for creating UUIDs that 293 | don't fragment indexes. Returns a UUID whose most significant 32 294 | bits are the current time in milliseconds, rounded to the nearest 295 | second." 296 | [] 297 | (let [seconds-hex (.toString (squuid-seconds-component) 16) 298 | trailing (.replace "-xxxx-4xxx-yxxx-xxxxxxxxxxxx" (js/RegExp. "[xy]" "g") 299 | (fn [c] 300 | (let [r (bit-or (* 16 (Math/random)) 0) 301 | v (if (= c "x") r (bit-or (bit-and r 0x3) 0x8))] 302 | (.toString v 16))))] 303 | (UUID. (str seconds-hex trailing)))) 304 | 305 | (defn squuid-time-millis 306 | "Get the time part of a squuid." 307 | [squuid] 308 | (-> (.-uuid squuid) 309 | (.slice 0 8) 310 | (js/parseInt 16) 311 | (* 1000))) 312 | 313 | ;; TODOs 314 | (comment 315 | 316 | ;; from datomic.api 317 | 318 | (defn touch 319 | [entity]) 320 | 321 | (defn filter 322 | [db pred]) 323 | 324 | (defn entid-at 325 | [db part t-or-date]) 326 | 327 | (defn entity-db 328 | [entity]) 329 | 330 | (defn next-t 331 | [db]) 332 | 333 | (defn part 334 | [eid]) 335 | 336 | (defn tx-report-queue 337 | "queue is a core.async channel" 338 | [conn]) 339 | 340 | (defn resolve-tempid 341 | [db tempids tempid]) 342 | 343 | (defn t->tx 344 | [t]) 345 | 346 | (defn tx->t 347 | [tx]) 348 | 349 | (defn tempid 350 | ([partition]) 351 | ([partition n])) 352 | 353 | ;; these might not be possible through the REST api 354 | (defn delete-database 355 | [...]) 356 | (defn rename-database 357 | [...]) 358 | (defn request-index 359 | [...]) 360 | 361 | ) 362 | --------------------------------------------------------------------------------