Skip to content

Instantly share code, notes, and snippets.

@currentoor
Created February 2, 2020 22:06
Show Gist options
  • Save currentoor/dce7e45a38563452c379c510be63f0a3 to your computer and use it in GitHub Desktop.
Save currentoor/dce7e45a38563452c379c510be63f0a3 to your computer and use it in GitHub Desktop.
(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