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))))))))