Skip to content

Commit

Permalink
generate honeysql param map from query input
Browse files Browse the repository at this point in the history
  • Loading branch information
nikolap committed May 28, 2023
1 parent 9b5e1e1 commit ad4e14d
Show file tree
Hide file tree
Showing 4 changed files with 118 additions and 54 deletions.
6 changes: 5 additions & 1 deletion src/route_craft/core.clj
Original file line number Diff line number Diff line change
Expand Up @@ -60,4 +60,8 @@

(ring/router
(generate-reitit-crud-routes
{:db-conn (jdbc/get-connection (:datasource ctx))})))
{:db-conn (jdbc/get-connection (:datasource ctx))})))


;; Future work:
;; - caching
7 changes: 3 additions & 4 deletions src/route_craft/handlers.clj
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
(ns route-craft.handlers
(:require [next.jdbc.sql :as sql]
[ring.util.http-response :as http-response]))
(:require
[next.jdbc.sql :as sql]
[ring.util.http-response :as http-response]))

;; ===================HANDLERS===================

Expand Down Expand Up @@ -145,8 +146,6 @@
;; - insert multi
;; - delete by query
;; - generic query
;; - query only specific columns
;; - resolve fks

(defmethod generate-handler :default
[_ handler]
Expand Down
135 changes: 86 additions & 49 deletions src/route_craft/querying.clj
Original file line number Diff line number Diff line change
@@ -1,51 +1,88 @@
(ns route-craft.querying)

;; Goal: Support JSON
;; Query string encodable
;; Secure
(ns route-craft.querying
(:require
[clojure.string :as string]))

#_{:where {:fk_table_id {:eq fk_table_id}}
:joins {:user_id "left"}
:limit limit
:offset offset
:columns ["id" ...]
:order [{:id "asc"}]}

(def valid-order [:enum "asc" "desc"])
(def valid-joins [:enum "left" "right" "inner" "full" "cross"])
(def valid-ops [:enum :eq :neq :ilike :gt :gte :lt :lte :in :nin])

(defn malli-query-params
[{:rc/keys [permitted-columns]}]
(let [columns-kw-enum (vec (cons :enum permitted-columns))
columns-str-enum (vec (cons :enum (map name permitted-columns)))]
[:map
[:where {:optional true} [:map-of columns-kw-enum [:map-of valid-ops any?]]]
[:joins {:optional true} [:map-of columns-kw-enum valid-joins]]
[:limit {:optional true} :int]
[:offset {:optional true} :int]
[:columns {:optional true} [:vector columns-str-enum]]
[:order {:optional true} [:vector [:map-of columns-kw-enum valid-order]]]]))

;; OPS
;; eq
;; neq
;; ilike
;; gt
;; gte
;; lt
;; lte
;; in
;; nin

;; table key
;; db-xray
;; refers-to permitted in joins
;; columns only permitted OR join columns only permitted
;; column checks on:
;; - where
;; - joins
;; - columns
;; - order keys
;; validate order vals as asc or desc
;; validate limit and offset as int
:joins {:user_id "left"}
:limit limit
:offset offset
:columns ["id" ...]
:order [{:id "asc"}]}

(defn qualified-column-kw
[base-table column]
(keyword
(if (string/includes? #"\." column)
column
(str base-table "." (name column)))))

(defn rc-columns->select
[columns]
(mapv qualified-column-kw columns))

(def honeysql-joins
{"left" :left-join
"right" :right-join
"inner" :inner-join
"full" :full-join
"cross" :cross-join})
(defn assoc-joins
[query-map {:keys [base-table columns]} joins]
(reduce-kv
(fn [out column-str join-type]
(let [column-kw (keyword column-str)]
(if-let [[target-table target-column] (get-in columns [column-kw :refers-to])]
(assoc out (get honeysql-joins join-type) [target-table [:=
(keyword (str (name target-table) (name target-column)))
(keyword (str (name base-table) (name column-kw)))]])
(throw (ex-info "No foreign key to join on" {:column column-kw
:type ::unsupported-column-join})))))
query-map
joins))

(def honeysql-ops
{:eq :=
:neq :not=
:like :like
:nlike :not-like
:ilike :ilike
:nilike :not-ilike
:gt :>
:gte :>=
:lt :<
:lte :<=
:in :in
:nin :not-in})

(defn rc-where->hsql-where
[{:keys [base-table]} where-map]
(reduce-kv
(fn [out column-key v]
(let [[op value] v]
(conj out
[(get honeysql-ops op)
(qualified-column-kw base-table column-key)
value])))
[:and]
where-map))

(defn rc-order->hsql-order-by
[{:keys [base-table]} order-map]
(reduce-kv
(fn [out column-kw order-direction]
(conj out [(qualified-column-kw base-table column-kw) (keyword order-direction)]))
[]
order-map))

(defn rc-query->query-map
"Given a validated route-craft query, convert to honeysql query map"
[{:keys [where joins limit offset columns order]} opts]
(cond-> {}
columns (assoc :select (rc-columns->select columns))
joins (assoc-joins opts joins)
where (assoc :where (rc-where->hsql-where opts where))
limit (assoc :limit limit)
offset (assoc :offset offset)
order (assoc :order-by (rc-order->hsql-order-by opts order))))

;; TODO: maybe this ns is candidate for memoization if enabled via config
24 changes: 24 additions & 0 deletions src/route_craft/routes.clj
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,30 @@
[clojure.tools.logging :as log]
[route-craft.handlers :as handlers]))

;; Custom query validation

(def valid-order [:enum
"asc"
"desc"
"asc-nulls-first"
"asc-nulls-last"
"desc-nulls-first"
"desc-nulls-last"])
(def valid-joins [:enum "left" "right" "inner" "full" "cross"])
(def valid-ops [:enum :eq :neq :like :nlike :ilike :nilike :gt :gte :lt :lte :in :nin])

(defn malli-query-params
[{:rc/keys [permitted-columns]}]
(let [columns-kw-enum (vec (cons :enum permitted-columns))
columns-str-enum (vec (cons :enum (map name permitted-columns)))]
[:map
[:where {:optional true} [:map-of columns-kw-enum [:map-of valid-ops any?]]]
[:joins {:optional true} [:map-of columns-kw-enum valid-joins]]
[:limit {:optional true} :int]
[:offset {:optional true} :int]
[:columns {:optional true} [:vector columns-str-enum]]
[:order {:optional true} [:vector [:map-of columns-kw-enum valid-order]]]]))

;; ===================ROUTING===================

;; generate crud routes using default handlers
Expand Down

0 comments on commit ad4e14d

Please sign in to comment.