Created
February 2, 2020 22:06
-
-
Save currentoor/dce7e45a38563452c379c510be63f0a3 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(ns ucv.lib.use-case | |
#?(:cljs (:require-macros [ucv.lib.use-case])) | |
(:require | |
[clojure.spec.alpha :as s] | |
[com.fulcrologic.fulcro.algorithms.do-not-use :as futil] | |
[taoensso.timbre :as log] | |
[com.wsscode.pathom.connect :as pc] | |
[ucv.util])) | |
(defonce pathom-registry (atom {})) | |
(defn register! [resolver] | |
(log/debug "Registering resolver" (::pc/sym resolver)) | |
(swap! pathom-registry assoc (::pc/sym resolver) resolver)) | |
(s/def ::env (s/and map? #(get % :db) #(get % :conn))) | |
(s/def ::policy (s/or :sym symbol? :expr list?)) | |
(s/def ::mutation-args (s/cat | |
:sym simple-symbol? | |
:doc (s/? string?) | |
:arglist vector? | |
:config map? | |
:body (s/* any?))) | |
#?(:clj | |
(defn defpathom-backend-endpoint* [endpoint args update-database?] | |
(let [{:keys [sym arglist doc config body]} (futil/conform! ::mutation-args args) | |
internal-fn-sym (symbol (str (name sym) "__internal-fn__")) | |
fqsym (if (namespace sym) | |
sym | |
(symbol (name (ns-name *ns*)) (name sym))) | |
{:keys [policy ex-return]} config | |
config (dissoc config :policy :ex-return) | |
env-arg (first arglist) | |
params-arg (second arglist) | |
ex-msg (str "Mutation " fqsym " unauthorized, " policy " violated")] | |
(when (nil? policy) | |
(throw (java.lang.IllegalArgumentException. "Config map MUST contain a :policy key"))) | |
`(do | |
;; Use this internal function so we can dynamically update a resolver in | |
;; dev without having to restart the whole pathom parser. | |
(defn ~internal-fn-sym [env# params#] | |
(if (nil? params#) | |
(do | |
(log/error (ex-info "Params are nil" {:params params#})) | |
{}) | |
(let [~env-arg (assoc env# :db (deref (:db-atom env#))) | |
~params-arg params# | |
result# (if (~policy {:env (assoc env# :db (deref (:db-atom env#))) :params params#}) | |
(try ~@body | |
(catch Throwable ex# | |
(log/error ex# ~(str fqsym) params#) | |
(let [ex-data# (or (ex-data ex#) {:message (.getMessage ex#) | |
:type (.getName (class ex#))}) | |
ex-return-result# ~ex-return | |
return# (if (or (map? ex-return-result#) (nil? ex-return-result#)) | |
ex-return-result# | |
{:ucv/mutation-errors {:error "ex-return was not a map!"}}) | |
return# (-> return# | |
(update :ucv/mutation-errors merge ex-data#))] | |
return#))) | |
(throw (ex-info ~ex-msg | |
(let [user# (:current/user env#) | |
firm# (:current/firm env#)] | |
(cond-> {:status 401 :params params#} | |
(:user/id user#) (assoc :user/id (:user/id user#)) | |
(:firm/id firm#) (assoc :firm/id (:firm/id firm#)))))))] | |
;; Override previous db value with a new one, in case this resolver | |
;; is being called from a mutation join. | |
(when ~update-database? | |
(reset! (:db-atom env#) (datomic.api/db (:conn env#)))) | |
;; Pathom doesn't expect nils | |
(cond | |
(sequential? result#) (vec (remove nil? result#)) | |
(nil? result#) {} | |
:else result#)))) | |
(~endpoint ~(cond-> sym | |
doc (with-meta {:doc doc})) [env# params#] | |
~config | |
(~internal-fn-sym env# params#)) | |
(ucv.lib.use-case/register! ~sym) | |
::done)))) | |
#?(:clj | |
(defn defpathom-controller-endpoint* [endpoint args] | |
(let [{:keys [sym arglist doc config body]} (futil/conform! ::mutation-args args) | |
fqsym (if (namespace sym) | |
sym | |
(symbol (name (ns-name *ns*)) (name sym))) | |
{:keys [ex-return]} config | |
config (dissoc config :ex-return) | |
env-arg (first arglist) | |
params-arg (second arglist)] | |
`(do | |
(~endpoint ~(cond-> sym | |
doc (with-meta {:doc doc})) [env# params#] | |
~config | |
(let [~env-arg env# | |
~params-arg params# | |
result# (try ~@body | |
(catch :default ex# | |
(log/error ~(str fqsym) "failed with:" ex# (str "'" (.getMessage ex#) "'") "input:" params#) | |
(let [ex-data# (ex-data ex#) | |
ex-return-result# ~ex-return | |
return# (if (or (map? ex-return-result#) (nil? ex-return-result#)) | |
ex-return-result# | |
{:ucv/mutation-errors {:error "ex-return was not a map!"}}) | |
return# (-> return# | |
(update :ucv/mutation-errors merge | |
(select-keys ex-data# [:alert/message])))] | |
return#)))] | |
result#)) | |
(ucv.lib.use-case/register! ~sym) | |
::done)))) | |
#?(:clj | |
(defmacro ^{:doc "Defines a server-side PATHOM mutation. | |
Example: | |
(defmutation do-thing | |
\"Optional docstring\" | |
[params] | |
{::pc/input [:param/name] ; PATHOM config (optional) | |
::pc/output [:result/prop]} | |
(with [env] ...) ; security policy (optional) | |
(action [env] ...)) ; actual action (required) | |
" | |
:arglists '([sym docstring? arglist config & body])} defmutation | |
[& args] | |
(if (boolean (:ns &env)) | |
(defpathom-controller-endpoint* `pc/defmutation args) | |
(defpathom-backend-endpoint* `pc/defmutation args true)))) | |
#?(:clj | |
(defmacro ^{:doc "Defines a pathom resolver but with authorization. | |
Example: | |
(defresolver resolver-name [env input] | |
{::pc/input [:customer/id] | |
:policy s.policy/ownership | |
...} | |
{:customer/name \"Bob\"}) | |
" | |
:arglists '([sym docstring? arglist config & body])} defresolver | |
[& args] | |
(if (boolean (:ns &env)) | |
(defpathom-controller-endpoint* `pc/defresolver args) | |
(defpathom-backend-endpoint* `pc/defresolver args false)))) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment