diff --git a/deps.edn b/deps.edn
index 8129df7..02dd0e9 100644
--- a/deps.edn
+++ b/deps.edn
@@ -1,7 +1,7 @@
{:deps {org.clojure/clojure {:mvn/version "1.11.3"}
org.clojure/core.async {:mvn/version "1.6.681"}
com.fluree/db {:git/url "https://github.com/fluree/db.git"
- :git/sha "f2bcf88ddf90c44ef50369da4edcb82f91f023e1"}
+ :git/sha "d86295512569010d6bcc057bd267aa4fdc0ecfe6"}
com.fluree/json-ld {:git/url "https://github.com/fluree/json-ld.git"
:git/sha "74083536c84d77f8cdd4b686b5661714010baad3"}
diff --git a/src/fluree/server/handler.clj b/src/fluree/server/handler.clj
index e0a943e..f389d98 100644
--- a/src/fluree/server/handler.clj
+++ b/src/fluree/server/handler.clj
@@ -198,6 +198,33 @@
:coercion ^:replace history-coercer
:handler #'ledger/history})
+(def fallback-handler
+ (let [swagger-ui-handler (swagger-ui/create-swagger-ui-handler
+ {:path "/"
+ :config {:validatorUrl nil
+ :operationsSorter "alpha"}})
+ default-handler (ring/create-default-handler)]
+ (ring/routes swagger-ui-handler default-handler)))
+
+(defn ledger-specific-handler
+ "Reroutes a ledger-specific request to the appropriate endpoint, adding the
+ `fluree-ledger` header so that the request is directed to the appropriate ledger."
+ [{{ledger-path :ledger-path} :path-params
+ :as req}]
+ (let [op-delimiter-idx (str/last-index-of ledger-path "/")
+
+ ;; deconstruct the path to obtain the ledger-alias and the endpoint
+ alias (subs ledger-path 0 op-delimiter-idx)
+ endpoint (str "/fluree" (subs ledger-path op-delimiter-idx))
+
+ ;; construct a new request to the correct endpoint with the fluree-ledger header
+ new-req (-> (dissoc req :reitit.core/match)
+ (update :headers assoc "fluree-ledger" alias)
+ (assoc :uri endpoint))
+ ;; create a new dynamic handler and re-route the new request.
+ re-route (ring/ring-handler (:reitit.core/router req) fallback-handler)]
+ (re-route new-req)))
+
(defn wrap-assoc-system
[conn consensus watcher subscriptions handler]
(fn [req]
@@ -299,6 +326,12 @@
(select-keys fluree-request-header-keys)
(update-keys (fn [k] (keyword (subs k request-header-prefix-count)))))
+ ;; SPARQL protocol headers for graph specification
+ from (get headers "default-graph-uri")
+ from-named (get headers "named-graph-uri")
+ using (get headers "using-graph-uri")
+ using-named (get headers "using-named-graph-uri")
+
max-fuel (when max-fuel
(try (Integer/parseInt max-fuel)
(catch Exception e
@@ -371,6 +404,10 @@
max-fuel (assoc :max-fuel max-fuel)
format (assoc :format format)
output (assoc :output output)
+ from (assoc :from from)
+ from-named (assoc :from from-named)
+ using (assoc :using using)
+ using-named (assoc :using using-named)
ledger (assoc :ledger ledger)
policy (assoc :policy policy)
policy-class (assoc :policy-class policy-class)
@@ -386,10 +423,6 @@
(assoc :fluree/opts opts)
handler))))
-(defn sort-middleware-by-weight
- [weighted-middleware]
- (map (fn [[_ mw]] mw) (sort-by first weighted-middleware)))
-
(def json-format
(mf/map->Format
{:name "application/json"
@@ -476,149 +509,103 @@
{::exception/default
(partial exception/wrap-log-to-console
fluree-exception-handler)}))]
- ;; Exception middleware should always be first AND last.
- ;; The last (highest sort order) one ensures that middleware that comes
- ;; after it will not be skipped on response if handler code throws an
- ;; exception b/c this it catches them and turns them into responses.
- ;; The first (lowest sort order) one ensures that exceptions thrown by
- ;; other middleware are caught and turned into appropriate responses.
- ;; Seems kind of clunky. Maybe there's a better way? - WSM 2023-04-28
- (sort-middleware-by-weight [[1 exception-middleware]
- [10 (partial wrap-cors cors-origins)]
- [10 (partial wrap-assoc-system connection consensus
- watcher subscriptions)]
- [50 unwrap-credential]
- [200 coercion/coerce-exceptions-middleware]
- [300 coercion/coerce-response-middleware]
- [400 coercion/coerce-request-middleware]
- [500 wrap-request-header-opts]
- [600 (wrap-closed-mode root-identities closed-mode)]
- [1000 exception-middleware]])))
-
-(def fluree-create-routes
- ["/create"
- {:post {:summary "Endpoint for creating new ledgers"
- :parameters {:body CreateRequestBody}
- :responses {201 {:body CreateResponseBody}
- 400 {:body ErrorResponse}
- 409 {:body ErrorResponse}
- 500 {:body ErrorResponse}}
- :handler #'create/default}}])
+ ;; Exception middleware should always be first AND last. The last one ensures that
+ ;; middleware that comes after it will not be skipped on response if handler code
+ ;; throws an exception b/c this it catches them and turns them into responses. The
+ ;; first one ensures that exceptions thrown by other middleware are caught and
+ ;; turned into appropriate responses. Seems kind of clunky. Maybe there's a better
+ ;; way? - WSM 2023-04-28
+ [exception-middleware
+ (partial wrap-cors cors-origins)
+ (partial wrap-assoc-system connection consensus
+ watcher subscriptions)
+ unwrap-credential
+ coercion/coerce-exceptions-middleware
+ coercion/coerce-response-middleware
+ coercion/coerce-request-middleware
+ wrap-request-header-opts
+ (wrap-closed-mode root-identities closed-mode)
+ exception-middleware]))
-(def fluree-drop-route
- ["/drop"
- {:post {:summary "Drop the specified ledger and delete all persisted artifacts."
- :parameters {:body DropRequestBody}
- :responses {200 {:body DropResponseBody}
+(def default-fluree-routes
+ ["/fluree"
+ ["/create"
+ {:post {:summary "Endpoint for creating new ledgers"
+ :parameters {:body CreateRequestBody}
+ :responses {201 {:body CreateResponseBody}
400 {:body ErrorResponse}
409 {:body ErrorResponse}
500 {:body ErrorResponse}}
- :coercion (rcm/create {:transformers {:body {:default rcm/json-transformer-provider}}})
- :handler #'drop/drop-handler}}])
-
-(def fluree-transact-routes
- ["/transact"
- {:post {:summary "Endpoint for submitting transactions"
- :parameters {:body TransactRequestBody}
- :responses {200 {:body TransactResponseBody}
+ :handler #'create/default}}]
+ ["/drop"
+ {:post {:summary "Drop the specified ledger and delete all persisted artifacts."
+ :parameters {:body DropRequestBody}
+ :responses {200 {:body DropResponseBody}
400 {:body ErrorResponse}
409 {:body ErrorResponse}
500 {:body ErrorResponse}}
- :handler #'srv-tx/update}}])
-
-(def fluree-update-route
- ["/update"
- {:post {:summary "Endpoint for submitting transactions"
- :parameters {:body TransactRequestBody}
- :responses {200 {:body TransactResponseBody}
+ :coercion (rcm/create {:transformers {:body {:default rcm/json-transformer-provider}}})
+ :handler #'drop/drop-handler}}]
+ ["/transact"
+ {:post {:summary "Endpoint for submitting transactions"
+ :parameters {:body TransactRequestBody}
+ :responses {200 {:body TransactResponseBody}
400 {:body ErrorResponse}
409 {:body ErrorResponse}
500 {:body ErrorResponse}}
- :handler #'srv-tx/update}}])
-
-(def fluree-insert-route
- ["/insert"
- {:post {:summary "Endpoint for inserting into the specified ledger."
- :parameters {:body :any}
- :responses {200 {:body TransactResponseBody}
+ :handler #'srv-tx/update}}]
+ ["/update"
+ {:post {:summary "Endpoint for submitting transactions"
+ :parameters {:body TransactRequestBody}
+ :responses {200 {:body TransactResponseBody}
400 {:body ErrorResponse}
409 {:body ErrorResponse}
500 {:body ErrorResponse}}
- :handler #'srv-tx/insert}}])
-
-(def fluree-upsert-route
- ["/upsert"
- {:post {:summary "Endpoint for upserting into the specified ledger."
- :parameters {:body :any}
- :responses {200 {:body TransactResponseBody}
+ :handler #'srv-tx/update}}]
+ ["/insert"
+ {:post {:summary "Endpoint for inserting into the specified ledger."
+ :parameters {:body :any}
+ :responses {200 {:body TransactResponseBody}
400 {:body ErrorResponse}
409 {:body ErrorResponse}
500 {:body ErrorResponse}}
- :handler #'srv-tx/upsert}}])
-
-(def fluree-query-routes
- ["/query"
- {:get query-endpoint
- :post query-endpoint}])
-
-(def fluree-history-routes
- ["/history"
- {:get history-endpoint
- :post history-endpoint}])
-
-(def fluree-remote-routes
- ["/remote"
- ["/latestCommit"
- {:post {:summary "Read latest commit for a ledger"
- :parameters {:body LatestCommitRequestBody}
- :handler #'remote/latest-commit}}]
- ["/resource"
- {:post {:summary "Read resource from address"
- :parameters {:body AddressRequestBody}
- :handler #'remote/read-resource-address}}]
- ["/hash"
- {:post {:summary "Parse content hash from address"
- :parameters {:body HashRequestBody}
- :handler #'remote/parse-address-hash}}]
- ["/addresses"
- {:post {:summary "Retrieve ledger address from alias"
- :parameters {:body AliasRequestBody}
- :handler #'remote/published-ledger-addresses}}]])
-
-(def fluree-subscription-routes
- ["/subscribe"
- {:get {:summary "Subscribe to ledger updates"
- :parameters {:body SubscriptionRequestBody}
- :handler #'subscription/default}}])
-
-(def default-fluree-route-map
- {:create fluree-create-routes
- :drop fluree-drop-route
- :insert fluree-insert-route
- :upsert fluree-upsert-route
- :update fluree-update-route
- :transact fluree-transact-routes
- :query fluree-query-routes
- :history fluree-history-routes
- :remote fluree-remote-routes
- :subscription fluree-subscription-routes})
-
-(defn combine-fluree-routes
- [fluree-route-map]
- (->> fluree-route-map
- vals
- (into ["/fluree"])))
-
-(def default-fluree-routes
- (combine-fluree-routes default-fluree-route-map))
-
-(def fallback-handler
- (let [swagger-ui-handler (swagger-ui/create-swagger-ui-handler
- {:path "/"
- :config {:validatorUrl nil
- :operationsSorter "alpha"}})
- default-handler (ring/create-default-handler)]
- (ring/routes swagger-ui-handler default-handler)))
+ :handler #'srv-tx/insert}}]
+
+ ["/upsert" {:post {:summary "Endpoint for upserting into the specified ledger."
+ :parameters {:body :any}
+ :responses {200 {:body TransactResponseBody}
+ 400 {:body ErrorResponse}
+ 409 {:body ErrorResponse}
+ 500 {:body ErrorResponse}}
+ :handler #'srv-tx/upsert}}]
+ ["/query" {:get query-endpoint
+ :post query-endpoint}]
+ ["/history" {:get history-endpoint
+ :post history-endpoint}]
+
+ ["/ledger/{*ledger-path}" #'ledger-specific-handler]
+
+ ["/subscribe"
+ {:get {:summary "Subscribe to ledger updates"
+ :parameters {:body SubscriptionRequestBody}
+ :handler #'subscription/default}}]
+ ["/remote"
+ ["/latestCommit"
+ {:post {:summary "Read latest commit for a ledger"
+ :parameters {:body LatestCommitRequestBody}
+ :handler #'remote/latest-commit}}]
+ ["/resource"
+ {:post {:summary "Read resource from address"
+ :parameters {:body AddressRequestBody}
+ :handler #'remote/read-resource-address}}]
+ ["/hash"
+ {:post {:summary "Parse content hash from address"
+ :parameters {:body HashRequestBody}
+ :handler #'remote/parse-address-hash}}]
+ ["/addresses"
+ {:post {:summary "Retrieve ledger address from alias"
+ :parameters {:body AliasRequestBody}
+ :handler #'remote/published-ledger-addresses}}]]])
(def swagger-routes
["/swagger.json"
@@ -649,10 +636,8 @@
([config]
(app config []))
([config custom-routes]
- (app config custom-routes default-fluree-routes))
- ([config custom-routes fluree-routes]
(let [app-middleware (compose-app-middleware config)
- app-routes (cond-> ["" {:middleware app-middleware} fluree-routes]
+ app-routes (cond-> ["" {:middleware app-middleware} default-fluree-routes]
(seq custom-routes) (conj custom-routes))
router (app-router app-routes)]
(ring/ring-handler router fallback-handler))))
diff --git a/src/fluree/server/handlers/ledger.clj b/src/fluree/server/handlers/ledger.clj
index c6150e1..5566a0a 100644
--- a/src/fluree/server/handlers/ledger.clj
+++ b/src/fluree/server/handlers/ledger.clj
@@ -5,11 +5,13 @@
[fluree.server.handlers.shared :refer [defhandler deref!] :as shared]))
(defhandler query
- [{:keys [fluree/conn fluree/opts] {:keys [body]} :parameters :as _req}]
+ [{:keys [fluree/conn fluree/opts] {:keys [body path]} :parameters :as _req}]
(let [query (or (::handler/query body) body)
+ ;; supply ledger-alias from path params if not overridden by a header
+ opts* (update opts :ledger #(or % (:ledger-alias path)))
{:keys [status result] :as query-response}
- (deref! (fluree/query-connection conn query opts))]
- (log/debug "query handler received query:" query opts)
+ (deref! (fluree/query-connection conn query opts*))]
+ (log/debug "query handler received query:" query opts*)
(shared/with-tracking-headers {:status status, :body result}
query-response)))
diff --git a/test/fluree/server/integration/basic_query_test.clj b/test/fluree/server/integration/basic_query_test.clj
index e072c9f..93b9ed6 100644
--- a/test/fluree/server/integration/basic_query_test.clj
+++ b/test/fluree/server/integration/basic_query_test.clj
@@ -201,6 +201,90 @@
["Leticia" 2 false]]
(-> query-res :body (json/parse false)))))))
+(deftest ledger-specific-request-test
+ (let [ledger-name "ledger-endpoint/basic-query-test"]
+ (testing "create"
+ (let [create-req1 {"ledger" ledger-name
+ "@context" {"ex" "http://example.com/"}
+ "insert" (mapv #(do {"@id" (str "ex:" %)
+ "@type" (if (odd? %) "ex:Odd" "ex:Even")
+ "ex:name" (str "name" %)
+ "ex:num" [(dec %) % (inc %)]
+ "ex:ref" {"@id" (str "ex:" (inc %))}})
+ (range 10))}
+ create-res1 (api-post :create {:body (json/stringify create-req1) :headers json-headers})
+
+ ledger-name2 (str ledger-name 2)
+ create-req2 {"ledger" ledger-name2
+ "@context" {"ex" "http://example.com/"}
+ "insert" (mapv #(do {"@id" (str "ex:" %)
+ "@type" (if (odd? %) "ex:Odd" "ex:Even")
+ "ex:name" (str "name" %)
+ "ex:num" [(dec %) % (inc %)]
+ "ex:ref" {"@id" (str "ex:" (inc %))}})
+ (range 10))}
+ create-res2 (api-post (str "/ledger/" ledger-name2 "/create") {:body (json/stringify create-req2) :headers json-headers})]
+ (is (= 201 (:status create-res1)))
+ (is (= 201 (:status create-res2)))))
+ (testing "transact"
+ (let [tx-req {"ledger" ledger-name
+ "@context" {"ex" "http://example.com/"}
+ "insert" {"@id" "ex:1" "ex:op" "transact"}}
+ tx-res (api-post (str "/ledger/" ledger-name "/transact") {:body (json/stringify tx-req) :headers json-headers})]
+ (is (= 200 (:status tx-res)))))
+ (testing "update"
+ (let [tx-req {"ledger" ledger-name
+ "@context" {"ex" "http://example.com/"}
+ "insert" {"@id" "ex:1" "ex:op" "update"}}
+ tx-res (api-post (str "/ledger/" ledger-name "/update") {:body (json/stringify tx-req) :headers json-headers})]
+ (is (= 200 (:status tx-res)))))
+ (testing "upsert"
+ (let [tx-req {"@context" {"ex" "http://example.com/"}
+ "@id" "ex:1" "ex:op2" "upsert"}
+ tx-res (api-post (str "/ledger/" ledger-name "/upsert") {:body (json/stringify tx-req) :headers json-headers})]
+ (is (= 200 (:status tx-res)))))
+ (testing "insert"
+ (let [tx-req {"@context" {"ex" "http://example.com/"}
+ "@id" "ex:1" "ex:op2" "insert"}
+ tx-res (api-post (str "/ledger/" ledger-name "/insert") {:body (json/stringify tx-req) :headers json-headers})]
+ (is (= 200 (:status tx-res)))))
+ (testing "query"
+ (let [query-req {"@context" {"ex" "http://example.com/"}
+ "select" {"ex:1" ["ex:op" "ex:op2"]}}
+ query-res (api-post (str "ledger/" ledger-name "/query")
+ {:body (json/stringify query-req) :headers json-headers})]
+ (is (= 200 (:status query-res)))
+ (is (= [{"ex:op" ["transact" "update"]
+ "ex:op2" ["insert" "upsert"]}]
+ (-> query-res :body (json/parse false))))))
+ (testing "history"
+ (let [query-req {"@context" {"ex" "http://example.com/"}
+ "from" ledger-name
+ "history" "ex:1"
+ "t" {"from" 2}}
+ query-res (api-post (str "ledger/" ledger-name "/history")
+ {:body (json/stringify query-req) :headers json-headers})]
+ (is (= 200 (:status query-res)))
+ (is (= [{"https://ns.flur.ee/ledger#t" 2,
+ "https://ns.flur.ee/ledger#assert" [{"@id" "ex:1", "ex:op" "transact"}],
+ "https://ns.flur.ee/ledger#retract" []}
+ {"https://ns.flur.ee/ledger#t" 3,
+ "https://ns.flur.ee/ledger#assert" [{"@id" "ex:1", "ex:op" "update"}],
+ "https://ns.flur.ee/ledger#retract" []}
+ {"https://ns.flur.ee/ledger#t" 4,
+ "https://ns.flur.ee/ledger#assert" [{"@id" "ex:1", "ex:op2" "upsert"}],
+ "https://ns.flur.ee/ledger#retract" []}
+ {"https://ns.flur.ee/ledger#t" 5,
+ "https://ns.flur.ee/ledger#assert" [{"@id" "ex:1", "ex:op2" "insert"}],
+ "https://ns.flur.ee/ledger#retract" []}]
+ (-> query-res :body (json/parse false))))))
+
+ (testing "nonmatching routes are handled uniformly"
+ (let [not-found1 (api-post "foo" {:body "{}" :headers json-headers})
+ not-found2 (api-post (str "ledger/" ledger-name "/foo") {:body "{}" :headers json-headers})]
+ (is (= 404 (:status not-found1)))
+ (is (= 404 (:status not-found2)))))))
+
#_(deftest ^:integration ^:edn query-edn-test
(testing "can query a basic entity w/ EDN"
(let [ledger-name (create-rand-ledger "query-endpoint-basic-entity-test")
diff --git a/test/fluree/server/integration/sparql_test.clj b/test/fluree/server/integration/sparql_test.clj
index 0412c1b..3793284 100644
--- a/test/fluree/server/integration/sparql_test.clj
+++ b/test/fluree/server/integration/sparql_test.clj
@@ -2,7 +2,7 @@
(:require [clojure.string :as str]
[clojure.test :refer [deftest is testing use-fixtures]]
[fluree.db.util.json :as json]
- [fluree.server.integration.test-system
+ [fluree.server.integration.test-system :as test-system
:refer [api-post create-rand-ledger run-test-server sparql-headers
sparql-results-headers sparql-update-headers]]))
@@ -63,3 +63,61 @@
"@graph" [{"@id" "ex:query-sparql-test"
"foo:name" ["query-sparql-test"]}]}
(-> rdf-res :body (json/parse false)))))))
+
+(deftest sparql-federated-query
+ (let [ledger-name "test-federated-query"
+ service (str "http://localhost:" @test-system/api-port "/fluree/ledger/" ledger-name "/query")
+
+ txn-req {"ledger" ledger-name
+ "@context" {"ex" "http://example.com/"}
+ "insert" (mapv #(do {"@id" (str "ex:" %)
+ "@type" (if (odd? %) "ex:Odd" "ex:Even")
+ "ex:name" (str "name" %)
+ "ex:num" [(dec %) % (inc %)]
+ "ex:ref" {"@id" (str "ex:" (inc %))}})
+ (range 10))}
+ txn-res (api-post :create {:body (json/stringify txn-req) :headers test-system/json-headers})]
+ (is (= 201 (:status txn-res)))
+ (testing "federated query can join across local and remote graphs"
+ (let [service-q (str/join "\n" ["PREFIX ex: "
+ "SELECT ?name ?type"
+ (str "FROM <" ledger-name ">")
+ "WHERE { ?s a ex:Even ; ex:ref ?ref . "
+ (str "SERVICE <" service "> { ?ref ex:name ?name ; a ?type . }")
+ "}"])
+ service-res (api-post :query {:body service-q :headers sparql-headers})]
+ (is (= 200 (:status service-res)))
+ (is (= [["name1" "ex:Odd"]
+ ["name3" "ex:Odd"]
+ ["name5" "ex:Odd"]
+ ["name7" "ex:Odd"]
+ ["name9" "ex:Odd"]]
+ (-> service-res :body (json/parse false))))))
+ (testing "SERVICE query with unavailable service returns an error"
+ (let [service-q (str/join "\n" ["PREFIX ex: "
+ "SELECT ?name ?type"
+ (str "FROM <" ledger-name ">")
+ "WHERE { ?s a ex:Even ; ex:ref ?ref . "
+ "SERVICE { ?ref ex:name ?name ; a ?type . }"
+ "}"])
+ service-res (api-post :query {:body service-q :headers sparql-headers})]
+ (is (= 400 (:status service-res)))
+ (is (= "Error executing query"
+ (-> service-res :body (json/parse false) (get "message"))))))
+ (testing "SERVICE query with unavailable service proceeds with SILENT"
+ (let [service-q (str/join "\n" ["PREFIX ex: "
+ "SELECT ?s ?type"
+ (str "FROM <" ledger-name ">")
+ "WHERE { ?s a ex:Even ; ex:ref ?ref . "
+ (str "SERVICE SILENT <"
+ (str/replace service ledger-name "foo/bar")
+ "> { ?ref ex:name ?name ; a ?type . }")
+ "}"])
+ service-res (api-post :query {:body service-q :headers sparql-headers})]
+ (is (= 200 (:status service-res)))
+ (is (= [["ex:0" nil]
+ ["ex:2" nil]
+ ["ex:4" nil]
+ ["ex:6" nil]
+ ["ex:8" nil]]
+ (-> service-res :body (json/parse false))))))))