Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 40 additions & 8 deletions src/fluree/server/handler.clj
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
[fluree.server.handlers.create :as create]
[fluree.server.handlers.drop :as drop]
[fluree.server.handlers.ledger :as ledger]
[fluree.server.handlers.ledger-resource :as ledger-resource]
[fluree.server.handlers.remote-resource :as remote]
[fluree.server.handlers.subscription :as subscription]
[fluree.server.handlers.transact :as srv-tx]
Expand Down Expand Up @@ -116,10 +117,10 @@
(def QueryFormat (m/schema [:enum :sparql]))

(def QueryRequestBody
(m/schema [:multi {:dispatch ::format}
(m/schema [:multi {:dispatch :sparql/format}
[:sparql [:map
[::query SparqlQuery]
[::format QueryFormat]]]
[:sparql/query SparqlQuery]
[:sparql/format QueryFormat]]]
[::m/default FqlQuery]]))

(def HistoryQueryResponse
Expand Down Expand Up @@ -387,9 +388,9 @@
(reify
mf/Decode
(decode [_ data charset]
{::query (String. (.readAllBytes ^InputStream data)
^String charset)
::format :sparql})))]}))
{:sparql/query (String. (.readAllBytes ^InputStream data)
^String charset)
:sparql/format :sparql})))]}))

(def sparql-update-format
(mf/map->Format
Expand Down Expand Up @@ -570,7 +571,23 @@
:parameters {:body SubscriptionRequestBody}
:handler #'subscription/default}}])

(def fluree-multi-query-routes
["/:query"
{:conflicting true ; Allow this to conflict with specific routes
:get query-endpoint
:post query-endpoint}])

(def fluree-ledger-resource-routes
["/{*ledger-path}"
{:conflicting true ; Allow this catch-all to conflict with specific routes
:middleware [ledger-resource/wrap-ledger-extraction]
:get {:summary "Ledger resource operations (GET)"
:handler #'ledger-resource/dispatch}
:post {:summary "Ledger resource operations (POST)"
:handler #'ledger-resource/dispatch}}])

(def default-fluree-route-map
"Legacy routes maintained for backwards compatibility"
{:create fluree-create-routes
:drop fluree-drop-route
:insert fluree-insert-route
Expand All @@ -582,15 +599,28 @@
:remote fluree-remote-routes
:subscription fluree-subscription-routes})

(def new-fluree-route-map
"New routes with ledger-specific resources and multi-ledger query"
{:multi-query fluree-multi-query-routes
:ledger-resource fluree-ledger-resource-routes})

(defn combine-fluree-routes
[fluree-route-map]
(->> fluree-route-map
vals
(into ["/fluree"])))

(def default-fluree-routes
"Legacy routes for backwards compatibility"
(combine-fluree-routes default-fluree-route-map))

(def all-fluree-routes
"Combined legacy and new routes - specific routes first, then catch-all routes"
(let [;; Order matters: specific routes must come before catch-all routes
specific-routes (vals default-fluree-route-map)
catch-all-routes (vals new-fluree-route-map)]
(into ["/fluree"] (concat specific-routes catch-all-routes))))

(def fallback-handler
(let [swagger-ui-handler (swagger-ui/create-swagger-ui-handler
{:path "/"
Expand Down Expand Up @@ -622,13 +652,15 @@
muuntaja-mw/format-request-middleware]]
(ring/router all-routes {:data {:coercion coercer
:muuntaja formatter
:middleware middleware}})))
:middleware middleware}
;; Allow conflicting routes - specific routes will be matched first
:conflicts nil})))

(defn app
([config]
(app config []))
([config custom-routes]
(app config custom-routes default-fluree-routes))
(app config custom-routes all-fluree-routes))
([config custom-routes fluree-routes]
(let [app-middleware (compose-app-middleware config)
app-routes (cond-> ["" {:middleware app-middleware} fluree-routes]
Expand Down
4 changes: 2 additions & 2 deletions src/fluree/server/handlers/ledger.clj
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
(ns fluree.server.handlers.ledger
(:require [fluree.db.api :as fluree]
[fluree.db.util.log :as log]
[fluree.server.handler :as-alias handler]
[fluree.server.handlers.shared :refer [defhandler deref!] :as shared]))

(defhandler query
[{:keys [fluree/conn fluree/opts] {:keys [body]} :parameters :as _req}]
(let [query (or (::handler/query body) body)
(let [query (or (:sparql/query body) body)
{:keys [status result] :as query-response}
(deref! (fluree/query-connection conn query opts))]
(log/debug "query handler received query:" query opts)
Expand All @@ -22,3 +21,4 @@
(if (and (map? result) (:status result) (:result result))
{:status (:status result), :body (:result result)}
{:status 200, :body result})))

74 changes: 74 additions & 0 deletions src/fluree/server/handlers/ledger_resource.clj
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
(ns fluree.server.handlers.ledger-resource
(:require [clojure.string :as str]
[fluree.db.util.log :as log]
[fluree.server.handlers.create :as create]
[fluree.server.handlers.drop :as drop]
[fluree.server.handlers.ledger :as ledger]
[fluree.server.handlers.shared :refer [defhandler]]
[fluree.server.handlers.transact :as tx]))

(set! *warn-on-reflection* true)

(def supported-operations
"Set of supported ledger operations as path suffixes"
#{":create" ":insert" ":upsert" ":update" ":delete"
":history" ":drop"})

(defn parse-ledger-operation
"Parse a path to extract ledger name and operation.
Walks backwards through path segments looking for a known operation.
Returns {:ledger-name '...' :operation :insert/:upsert/etc}

Examples:
'my-ledger/:insert' -> {:ledger-name 'my-ledger' :operation :insert}
'my/nested/ledger/:update' -> {:ledger-name 'my/nested/ledger' :operation :update}
'my-ledger' -> {:ledger-name 'my-ledger' :operation nil}"
[path]
(let [path-parts (str/split path #"/")
reversed-parts (reverse path-parts)]
(loop [parts reversed-parts
seen-parts []]
(if-let [part (first parts)]
(if (supported-operations part)
;; Found an operation, everything before it is the ledger name
{:ledger-name (str/join "/" (reverse (rest parts)))
:operation (keyword (subs part 1))}
;; Keep looking, accumulating parts
(recur (rest parts) (conj seen-parts part)))
;; No operation found, entire path is ledger name
{:ledger-name (str/join "/" (reverse seen-parts))
:operation nil}))))

(defn wrap-ledger-extraction
"Middleware to extract ledger name and operation from path.
Adds :ledger to fluree/opts and :fluree/operation to request."
[handler]
(fn [req]
(if-let [ledger-path (get-in req [:parameters :path :ledger-path])]
(let [{:keys [ledger-name operation]} (parse-ledger-operation ledger-path)]
(log/debug "Extracted ledger:" ledger-name "operation:" operation "from path:" ledger-path)
(-> req
(assoc-in [:fluree/opts :ledger] ledger-name)
(assoc :fluree/operation operation)
handler))
(handler req))))

(defhandler dispatch
[{:keys [fluree/operation] :as req}]
(log/debug "Dispatching ledger resource operation:" operation)
(case operation
:create (create/default req)
:insert (tx/insert req)
:upsert (tx/upsert req)
:update (tx/update req)
:delete (tx/update req) ; delete is just an update operation
:drop (let [ledger-name (get-in req [:fluree/opts :ledger])]
(drop/drop-handler (assoc-in req [:parameters :body :ledger] ledger-name)))
:history (let [ledger-name (get-in req [:fluree/opts :ledger])]
(ledger/history (assoc-in req [:parameters :body :from] ledger-name)))
;; No operation specified - default to query for GET, error for POST
(if (= :get (:request-method req))
(ledger/query req)
(throw (ex-info "Operation required for POST requests to ledger resource"
{:status 400
:error :db/invalid-operation})))))
31 changes: 31 additions & 0 deletions test/fluree/server/handlers/ledger_resource_test.clj
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
(ns fluree.server.handlers.ledger-resource-test
(:require [clojure.test :refer [deftest is testing]]
[fluree.server.handlers.ledger-resource :as lr]))

(deftest parse-ledger-operation-test
(testing "Parse ledger operation from path"
(testing "Simple ledger with operation"
(is (= {:ledger-name "my-ledger" :operation :insert}
(lr/parse-ledger-operation "my-ledger/:insert"))))

(testing "Nested ledger path with operation"
(is (= {:ledger-name "my/nested/ledger" :operation :update}
(lr/parse-ledger-operation "my/nested/ledger/:update"))))

(testing "Ledger path without operation"
(is (= {:ledger-name "my-ledger" :operation nil}
(lr/parse-ledger-operation "my-ledger"))))

(testing "Complex ledger name with slashes"
(is (= {:ledger-name "org/project/dataset" :operation :history}
(lr/parse-ledger-operation "org/project/dataset/:history"))))

(testing "All supported operations"
(doseq [op [:create :insert :upsert :update :delete :history :drop]]
(let [path (str "test-ledger/:" (name op))]
(is (= {:ledger-name "test-ledger" :operation op}
(lr/parse-ledger-operation path))))))

(testing "Ledger name with multiple slashes and operation"
(is (= {:ledger-name "a/b/c/d/e" :operation :history}
(lr/parse-ledger-operation "a/b/c/d/e/:history"))))))