mirror of
https://github.com/penpot/penpot.git
synced 2025-05-14 09:46:37 +02:00
🎉 Add better error reporting.
This commit is contained in:
parent
a881d86637
commit
4d7a34a998
14 changed files with 238 additions and 72 deletions
|
@ -34,6 +34,7 @@
|
||||||
org.postgresql/postgresql {:mvn/version "42.2.16"}
|
org.postgresql/postgresql {:mvn/version "42.2.16"}
|
||||||
com.zaxxer/HikariCP {:mvn/version "3.4.5"}
|
com.zaxxer/HikariCP {:mvn/version "3.4.5"}
|
||||||
|
|
||||||
|
funcool/log4j2-clojure {:mvn/version "2020.11.23-1"}
|
||||||
funcool/datoteka {:mvn/version "1.2.0"}
|
funcool/datoteka {:mvn/version "1.2.0"}
|
||||||
funcool/promesa {:mvn/version "5.1.0"}
|
funcool/promesa {:mvn/version "5.1.0"}
|
||||||
funcool/cuerdas {:mvn/version "2020.03.26-3"}
|
funcool/cuerdas {:mvn/version "2020.03.26-3"}
|
||||||
|
|
|
@ -12,6 +12,10 @@
|
||||||
</Policies>
|
</Policies>
|
||||||
<DefaultRolloverStrategy max="9"/>
|
<DefaultRolloverStrategy max="9"/>
|
||||||
</RollingFile>
|
</RollingFile>
|
||||||
|
|
||||||
|
<CljFn name="error-reporter" ns="app.error-reporter" fn="enqueue">
|
||||||
|
<PatternLayout pattern="[%d{YYYY-MM-dd HH:mm:ss.SSS}] [%t] %level{length=1} %logger{36} - %msg%n"/>
|
||||||
|
</CljFn>
|
||||||
</Appenders>
|
</Appenders>
|
||||||
|
|
||||||
<Loggers>
|
<Loggers>
|
||||||
|
@ -23,13 +27,17 @@
|
||||||
<AppenderRef ref="console"/>
|
<AppenderRef ref="console"/>
|
||||||
</Logger>
|
</Logger>
|
||||||
|
|
||||||
|
<Logger name="app.error-reporter" level="debug" additivity="false">
|
||||||
|
<AppenderRef ref="console"/>
|
||||||
|
</Logger>
|
||||||
|
|
||||||
<Logger name="app" level="debug" additivity="false">
|
<Logger name="app" level="debug" additivity="false">
|
||||||
<AppenderRef ref="main" level="debug" />
|
<AppenderRef ref="main" level="debug" />
|
||||||
|
<AppenderRef ref="error-reporter" level="error" />
|
||||||
</Logger>
|
</Logger>
|
||||||
|
|
||||||
<Root level="info">
|
<Root level="info">
|
||||||
<AppenderRef ref="main" />
|
<AppenderRef ref="main" />
|
||||||
<!-- <AppenderRef ref="console" /> -->
|
|
||||||
</Root>
|
</Root>
|
||||||
</Loggers>
|
</Loggers>
|
||||||
</Configuration>
|
</Configuration>
|
||||||
|
|
|
@ -40,6 +40,8 @@
|
||||||
:smtp-default-reply-to "no-reply@example.com"
|
:smtp-default-reply-to "no-reply@example.com"
|
||||||
:smtp-default-from "no-reply@example.com"
|
:smtp-default-from "no-reply@example.com"
|
||||||
|
|
||||||
|
:host "devenv"
|
||||||
|
|
||||||
:allow-demo-users true
|
:allow-demo-users true
|
||||||
:registration-enabled true
|
:registration-enabled true
|
||||||
:registration-domain-whitelist ""
|
:registration-domain-whitelist ""
|
||||||
|
@ -78,6 +80,9 @@
|
||||||
(s/def ::media-uri ::us/string)
|
(s/def ::media-uri ::us/string)
|
||||||
(s/def ::media-directory ::us/string)
|
(s/def ::media-directory ::us/string)
|
||||||
(s/def ::secret-key ::us/string)
|
(s/def ::secret-key ::us/string)
|
||||||
|
|
||||||
|
(s/def ::host ::us/string)
|
||||||
|
(s/def ::error-report-webhook ::us/string)
|
||||||
(s/def ::smtp-enabled ::us/boolean)
|
(s/def ::smtp-enabled ::us/boolean)
|
||||||
(s/def ::smtp-default-reply-to ::us/email)
|
(s/def ::smtp-default-reply-to ::us/email)
|
||||||
(s/def ::smtp-default-from ::us/email)
|
(s/def ::smtp-default-from ::us/email)
|
||||||
|
@ -135,6 +140,7 @@
|
||||||
::assets-uri
|
::assets-uri
|
||||||
::media-directory
|
::media-directory
|
||||||
::media-uri
|
::media-uri
|
||||||
|
::error-report-webhook
|
||||||
::secret-key
|
::secret-key
|
||||||
::smtp-default-from
|
::smtp-default-from
|
||||||
::smtp-default-reply-to
|
::smtp-default-reply-to
|
||||||
|
@ -145,6 +151,7 @@
|
||||||
::smtp-password
|
::smtp-password
|
||||||
::smtp-tls
|
::smtp-tls
|
||||||
::smtp-ssl
|
::smtp-ssl
|
||||||
|
::host
|
||||||
::file-trimming-threshold
|
::file-trimming-threshold
|
||||||
::debug-humanize-transit
|
::debug-humanize-transit
|
||||||
::allow-demo-users
|
::allow-demo-users
|
||||||
|
|
83
backend/src/app/error_reporter.clj
Normal file
83
backend/src/app/error_reporter.clj
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
;;
|
||||||
|
;; This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||||
|
;; defined by the Mozilla Public License, v. 2.0.
|
||||||
|
;;
|
||||||
|
;; Copyright (c) 2020 Andrey Antukh <niwi@niwi.nz>
|
||||||
|
|
||||||
|
(ns app.error-reporter
|
||||||
|
"A mattermost integration for error reporting."
|
||||||
|
(:require
|
||||||
|
[app.common.exceptions :as ex]
|
||||||
|
[app.common.spec :as us]
|
||||||
|
[app.config :as cfg]
|
||||||
|
[app.db :as db]
|
||||||
|
[app.tasks :as tasks]
|
||||||
|
[app.util.async :as aa]
|
||||||
|
[app.worker :as wrk]
|
||||||
|
[app.util.http :as http]
|
||||||
|
[clojure.core.async :as a]
|
||||||
|
[clojure.data.json :as json]
|
||||||
|
[clojure.spec.alpha :as s]
|
||||||
|
[clojure.tools.logging :as log]
|
||||||
|
[cuerdas.core :as str]
|
||||||
|
[mount.core :as mount :refer [defstate]]
|
||||||
|
[promesa.exec :as px]))
|
||||||
|
|
||||||
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
;; Public API
|
||||||
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
|
||||||
|
(defonce enqueue identity)
|
||||||
|
|
||||||
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
;; Implementation
|
||||||
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
|
||||||
|
(defn- send-to-mattermost!
|
||||||
|
[log-event]
|
||||||
|
(try
|
||||||
|
(let [text (str/fmt "Unhandled exception: `host='%s'`, `version=%s`.\n@channel ⇊\n```%s\n```"
|
||||||
|
(:host cfg/config)
|
||||||
|
(:full @cfg/version)
|
||||||
|
(str log-event))
|
||||||
|
rsp (http/send! {:uri (:error-reporter-webhook cfg/config)
|
||||||
|
:method :post
|
||||||
|
:headers {"content-type" "application/json"}
|
||||||
|
:body (json/write-str {:text text})})]
|
||||||
|
(when (not= (:status rsp) 200)
|
||||||
|
(log/warnf "Error reporting webhook replying with unexpected status: %s\n%s"
|
||||||
|
(:status rsp)
|
||||||
|
(pr-str rsp))))
|
||||||
|
(catch Exception e
|
||||||
|
(log/warnf e "Unexpected exception on error reporter."))))
|
||||||
|
|
||||||
|
(defn- send!
|
||||||
|
[val]
|
||||||
|
(aa/thread-call wrk/executor (partial send-to-mattermost! val)))
|
||||||
|
|
||||||
|
(defn- start
|
||||||
|
[]
|
||||||
|
(let [qch (a/chan (a/sliding-buffer 128))]
|
||||||
|
(log/info "Starting error reporter loop.")
|
||||||
|
|
||||||
|
;; Only enable when a valid URL is provided.
|
||||||
|
(when (:error-reporter-webhook cfg/config)
|
||||||
|
(alter-var-root #'enqueue (constantly #(a/>!! qch %)))
|
||||||
|
(a/go-loop []
|
||||||
|
(let [val (a/<! qch)]
|
||||||
|
(if (nil? val)
|
||||||
|
(do
|
||||||
|
(log/info "Closing error reporting loop.")
|
||||||
|
(alter-var-root #'enqueue (constantly identity)))
|
||||||
|
(do
|
||||||
|
(a/<! (send! val))
|
||||||
|
(recur))))))
|
||||||
|
|
||||||
|
qch))
|
||||||
|
|
||||||
|
(defstate reporter
|
||||||
|
:start (start)
|
||||||
|
:stop (a/close! reporter))
|
|
@ -30,8 +30,8 @@
|
||||||
(rring/router
|
(rring/router
|
||||||
[["/metrics" {:get mtx/dump}]
|
[["/metrics" {:get mtx/dump}]
|
||||||
["/api" {:middleware [[middleware/format-response-body]
|
["/api" {:middleware [[middleware/format-response-body]
|
||||||
[middleware/errors errors/handle]
|
|
||||||
[middleware/parse-request-body]
|
[middleware/parse-request-body]
|
||||||
|
[middleware/errors errors/handle]
|
||||||
[middleware/params]
|
[middleware/params]
|
||||||
[middleware/multipart-params]
|
[middleware/multipart-params]
|
||||||
[middleware/keyword-params]
|
[middleware/keyword-params]
|
||||||
|
|
|
@ -10,13 +10,16 @@
|
||||||
(ns app.http.errors
|
(ns app.http.errors
|
||||||
"A errors handling for the http server."
|
"A errors handling for the http server."
|
||||||
(:require
|
(:require
|
||||||
|
[app.common.exceptions :as ex]
|
||||||
[clojure.tools.logging :as log]
|
[clojure.tools.logging :as log]
|
||||||
[cuerdas.core :as str]))
|
[cuerdas.core :as str]
|
||||||
|
[expound.alpha :as expound]))
|
||||||
|
|
||||||
(defmulti handle-exception
|
(defmulti handle-exception
|
||||||
(fn [err & _rest]
|
(fn [err & _rest]
|
||||||
(:type (ex-data err))))
|
(let [edata (ex-data err)]
|
||||||
|
(or (:type edata)
|
||||||
|
(class err)))))
|
||||||
|
|
||||||
(defmethod handle-exception :authorization
|
(defmethod handle-exception :authorization
|
||||||
[err _]
|
[err _]
|
||||||
|
@ -26,17 +29,19 @@
|
||||||
(defmethod handle-exception :validation
|
(defmethod handle-exception :validation
|
||||||
[err req]
|
[err req]
|
||||||
(let [header (get-in req [:headers "accept"])
|
(let [header (get-in req [:headers "accept"])
|
||||||
response (ex-data err)]
|
edata (ex-data err)]
|
||||||
(cond
|
(cond
|
||||||
(and (str/starts-with? header "text/html")
|
(and (str/starts-with? header "text/html")
|
||||||
(= :spec-validation (:code response)))
|
(= :spec-validation (:code edata)))
|
||||||
{:status 400
|
{:status 400
|
||||||
:headers {"content-type" "text/html"}
|
:headers {"content-type" "text/html"}
|
||||||
:body (str "<pre style='font-size:16px'>" (:explain response) "</pre>\n")}
|
:body (str "<pre style='font-size:16px'>"
|
||||||
|
(with-out-str
|
||||||
|
(:data edata))
|
||||||
|
"</pre>\n")}
|
||||||
:else
|
:else
|
||||||
{:status 400
|
{:status 400
|
||||||
:body response})))
|
:body edata})))
|
||||||
|
|
||||||
(defmethod handle-exception :ratelimit
|
(defmethod handle-exception :ratelimit
|
||||||
[_ _]
|
[_ _]
|
||||||
|
@ -60,11 +65,38 @@
|
||||||
:body {:type :parse
|
:body {:type :parse
|
||||||
:message (ex-message err)}})
|
:message (ex-message err)}})
|
||||||
|
|
||||||
|
(defn get-context-string
|
||||||
|
[err request]
|
||||||
|
(str
|
||||||
|
"=| uri: " (pr-str (:uri request)) "\n"
|
||||||
|
"=| method: " (pr-str (:request-method request)) "\n"
|
||||||
|
"=| path-params: " (pr-str (:path-params request)) "\n"
|
||||||
|
"=| query-params: " (pr-str (:query-params request)) "\n"
|
||||||
|
|
||||||
|
(when-let [bparams (:body-params request)]
|
||||||
|
(str "=| body-params: " (pr-str bparams) "\n"))
|
||||||
|
|
||||||
|
(when (ex/ex-info? err)
|
||||||
|
(str "=| ex-data: " (pr-str (ex-data err)) "\n"))
|
||||||
|
|
||||||
|
"\n"))
|
||||||
|
|
||||||
|
|
||||||
|
(defmethod handle-exception :assertion
|
||||||
|
[err request]
|
||||||
|
(let [{:keys [data] :as edata} (ex-data err)]
|
||||||
|
(log/errorf err
|
||||||
|
(str "Assertion error\n"
|
||||||
|
(get-context-string err request)
|
||||||
|
(with-out-str (expound/printer data))))
|
||||||
|
{:status 500
|
||||||
|
:body {:type :internal-error
|
||||||
|
:message "Assertion error"
|
||||||
|
:data (ex-data err)}}))
|
||||||
|
|
||||||
(defmethod handle-exception :default
|
(defmethod handle-exception :default
|
||||||
[err req]
|
[err request]
|
||||||
(log/error "Unhandled exception on request:" (:path req) "\n"
|
(log/errorf err (str "Internal Error\n" (get-context-string err request)))
|
||||||
(with-out-str
|
|
||||||
(.printStackTrace ^Throwable err (java.io.PrintWriter. *out*))))
|
|
||||||
{:status 500
|
{:status 500
|
||||||
:body {:type :internal-error
|
:body {:type :internal-error
|
||||||
:message (ex-message err)
|
:message (ex-message err)
|
||||||
|
|
|
@ -9,6 +9,10 @@
|
||||||
|
|
||||||
(ns app.http.handlers
|
(ns app.http.handlers
|
||||||
(:require
|
(:require
|
||||||
|
[app.common.data :as d]
|
||||||
|
[app.common.exceptions :as ex]
|
||||||
|
[app.emails :as emails]
|
||||||
|
[app.http.session :as session]
|
||||||
[app.services.init]
|
[app.services.init]
|
||||||
[app.services.mutations :as sm]
|
[app.services.mutations :as sm]
|
||||||
[app.services.queries :as sq]))
|
[app.services.queries :as sq]))
|
||||||
|
@ -25,36 +29,40 @@
|
||||||
:login})
|
:login})
|
||||||
|
|
||||||
(defn query-handler
|
(defn query-handler
|
||||||
[req]
|
[{:keys [profile-id] :as request}]
|
||||||
(let [type (keyword (get-in req [:path-params :type]))
|
(let [type (keyword (get-in request [:path-params :type]))
|
||||||
data (merge (:params req)
|
data (assoc (:params request) ::sq/type type)
|
||||||
{::sq/type type})
|
data (if profile-id
|
||||||
data (cond-> data
|
(assoc data :profile-id profile-id)
|
||||||
(:profile-id req) (assoc :profile-id (:profile-id req)))]
|
(dissoc data :profile-id))]
|
||||||
(if (or (:profile-id req) (contains? unauthorized-services type))
|
|
||||||
|
(if (or (uuid? profile-id)
|
||||||
|
(contains? unauthorized-services type))
|
||||||
{:status 200
|
{:status 200
|
||||||
:body (sq/handle (with-meta data {:req req}))}
|
:body (sq/handle (with-meta data {:req request}))}
|
||||||
{:status 403
|
{:status 403
|
||||||
:body {:type :authentication
|
:body {:type :authentication
|
||||||
:code :unauthorized}})))
|
:code :unauthorized}})))
|
||||||
|
|
||||||
(defn mutation-handler
|
(defn mutation-handler
|
||||||
[req]
|
[{:keys [profile-id] :as request}]
|
||||||
(let [type (keyword (get-in req [:path-params :type]))
|
(let [type (keyword (get-in request [:path-params :type]))
|
||||||
data (merge (:params req)
|
data (d/merge (:params request)
|
||||||
(:body-params req)
|
(:body-params request)
|
||||||
(:uploads req)
|
(:uploads request)
|
||||||
{::sm/type type})
|
{::sm/type type})
|
||||||
data (cond-> data
|
data (if profile-id
|
||||||
(:profile-id req) (assoc :profile-id (:profile-id req)))]
|
(assoc data :profile-id profile-id)
|
||||||
(if (or (:profile-id req) (contains? unauthorized-services type))
|
(dissoc data :profile-id))]
|
||||||
(let [result (sm/handle (with-meta data {:req req}))
|
|
||||||
|
(if (or (uuid? profile-id)
|
||||||
|
(contains? unauthorized-services type))
|
||||||
|
(let [result (sm/handle (with-meta data {:req request}))
|
||||||
mdata (meta result)
|
mdata (meta result)
|
||||||
resp {:status (if (nil? (seq result)) 204 200)
|
resp {:status (if (nil? (seq result)) 204 200)
|
||||||
:body result}]
|
:body result}]
|
||||||
(cond->> resp
|
(cond->> resp
|
||||||
(:transform-response mdata) ((:transform-response mdata) req)))
|
(:transform-response mdata) ((:transform-response mdata) request)))
|
||||||
|
|
||||||
{:status 403
|
{:status 403
|
||||||
:body {:type :authentication
|
:body {:type :authentication
|
||||||
:code :unauthorized}})))
|
:code :unauthorized}})))
|
||||||
|
|
43
backend/src/app/services.clj
Normal file
43
backend/src/app/services.clj
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
;;
|
||||||
|
;; This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||||
|
;; defined by the Mozilla Public License, v. 2.0.
|
||||||
|
;;
|
||||||
|
;; Copyright (c) 2020 UXBOX Labs SL
|
||||||
|
|
||||||
|
(ns app.services
|
||||||
|
"A initialization of services."
|
||||||
|
(:require
|
||||||
|
[app.services.middleware :as middleware]
|
||||||
|
[app.util.dispatcher :as uds]
|
||||||
|
[mount.core :as mount :refer [defstate]]))
|
||||||
|
|
||||||
|
;; --- Initialization
|
||||||
|
|
||||||
|
(defn- load-query-services
|
||||||
|
[]
|
||||||
|
(require 'app.services.queries.projects)
|
||||||
|
(require 'app.services.queries.files)
|
||||||
|
(require 'app.services.queries.comments)
|
||||||
|
(require 'app.services.queries.profile)
|
||||||
|
(require 'app.services.queries.recent-files)
|
||||||
|
(require 'app.services.queries.viewer))
|
||||||
|
|
||||||
|
(defn- load-mutation-services
|
||||||
|
[]
|
||||||
|
(require 'app.services.mutations.demo)
|
||||||
|
(require 'app.services.mutations.media)
|
||||||
|
(require 'app.services.mutations.projects)
|
||||||
|
(require 'app.services.mutations.files)
|
||||||
|
(require 'app.services.mutations.comments)
|
||||||
|
(require 'app.services.mutations.profile)
|
||||||
|
(require 'app.services.mutations.viewer)
|
||||||
|
(require 'app.services.mutations.verify-token))
|
||||||
|
|
||||||
|
(defstate query-services
|
||||||
|
:start (load-query-services))
|
||||||
|
|
||||||
|
(defstate mutation-services
|
||||||
|
:start (load-mutation-services))
|
|
@ -7,33 +7,4 @@
|
||||||
;;
|
;;
|
||||||
;; Copyright (c) 2020 UXBOX Labs SL
|
;; Copyright (c) 2020 UXBOX Labs SL
|
||||||
|
|
||||||
(ns app.services.init
|
(ns app.services.init)
|
||||||
"A initialization of services."
|
|
||||||
(:require
|
|
||||||
[mount.core :as mount :refer [defstate]]))
|
|
||||||
|
|
||||||
(defn- load-query-services
|
|
||||||
[]
|
|
||||||
(require 'app.services.queries.projects)
|
|
||||||
(require 'app.services.queries.files)
|
|
||||||
(require 'app.services.queries.comments)
|
|
||||||
(require 'app.services.queries.profile)
|
|
||||||
(require 'app.services.queries.recent-files)
|
|
||||||
(require 'app.services.queries.viewer))
|
|
||||||
|
|
||||||
(defn- load-mutation-services
|
|
||||||
[]
|
|
||||||
(require 'app.services.mutations.demo)
|
|
||||||
(require 'app.services.mutations.media)
|
|
||||||
(require 'app.services.mutations.projects)
|
|
||||||
(require 'app.services.mutations.files)
|
|
||||||
(require 'app.services.mutations.comments)
|
|
||||||
(require 'app.services.mutations.profile)
|
|
||||||
(require 'app.services.mutations.viewer)
|
|
||||||
(require 'app.services.mutations.verify-token))
|
|
||||||
|
|
||||||
(defstate query-services
|
|
||||||
:start (load-query-services))
|
|
||||||
|
|
||||||
(defstate mutation-services
|
|
||||||
:start (load-mutation-services))
|
|
||||||
|
|
|
@ -380,7 +380,7 @@
|
||||||
(defn thread-pool
|
(defn thread-pool
|
||||||
([] (thread-pool {}))
|
([] (thread-pool {}))
|
||||||
([{:keys [min-threads max-threads name]
|
([{:keys [min-threads max-threads name]
|
||||||
:or {min-threads 0 max-threads 128}}]
|
:or {min-threads 0 max-threads 256}}]
|
||||||
(let [executor (QueuedThreadPool. max-threads min-threads)]
|
(let [executor (QueuedThreadPool. max-threads min-threads)]
|
||||||
(.setName executor (or name "default-tp"))
|
(.setName executor (or name "default-tp"))
|
||||||
(.start executor)
|
(.start executor)
|
||||||
|
|
|
@ -17,7 +17,7 @@
|
||||||
[mount.core :as mount]
|
[mount.core :as mount]
|
||||||
[environ.core :refer [env]]
|
[environ.core :refer [env]]
|
||||||
[app.common.pages :as cp]
|
[app.common.pages :as cp]
|
||||||
[app.services.init]
|
[app.services]
|
||||||
[app.services.mutations.profile :as profile]
|
[app.services.mutations.profile :as profile]
|
||||||
[app.services.mutations.projects :as projects]
|
[app.services.mutations.projects :as projects]
|
||||||
[app.services.mutations.teams :as teams]
|
[app.services.mutations.teams :as teams]
|
||||||
|
@ -50,8 +50,8 @@
|
||||||
#'app.redis/client
|
#'app.redis/client
|
||||||
#'app.redis/conn
|
#'app.redis/conn
|
||||||
#'app.media/semaphore
|
#'app.media/semaphore
|
||||||
#'app.services.init/query-services
|
#'app.services/query-services
|
||||||
#'app.services.init/mutation-services
|
#'app.services/mutation-services
|
||||||
#'app.migrations/migrations
|
#'app.migrations/migrations
|
||||||
#'app.media-storage/assets-storage
|
#'app.media-storage/assets-storage
|
||||||
#'app.media-storage/media-storage})
|
#'app.media-storage/media-storage})
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
|
|
||||||
(ns app.common.data
|
(ns app.common.data
|
||||||
"Data manipulation and query helper functions."
|
"Data manipulation and query helper functions."
|
||||||
(:refer-clojure :exclude [concat read-string hash-map])
|
(:refer-clojure :exclude [concat read-string hash-map merge])
|
||||||
#?(:cljs
|
#?(:cljs
|
||||||
(:require-macros [app.common.data]))
|
(:require-macros [app.common.data]))
|
||||||
(:require
|
(:require
|
||||||
|
@ -210,6 +210,17 @@
|
||||||
(assoc m key v)
|
(assoc m key v)
|
||||||
m)))
|
m)))
|
||||||
|
|
||||||
|
(defn merge
|
||||||
|
"A faster merge."
|
||||||
|
[& maps]
|
||||||
|
(loop [res (transient (first maps))
|
||||||
|
maps (next maps)]
|
||||||
|
(if (nil? maps)
|
||||||
|
(persistent! res)
|
||||||
|
(recur (reduce-kv assoc! res (first maps))
|
||||||
|
(next maps)))))
|
||||||
|
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
;; Data Parsing / Conversion
|
;; Data Parsing / Conversion
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
|
|
@ -48,3 +48,7 @@
|
||||||
(defmacro try
|
(defmacro try
|
||||||
[& exprs]
|
[& exprs]
|
||||||
`(try* (^:once fn* [] ~@exprs) identity))
|
`(try* (^:once fn* [] ~@exprs) identity))
|
||||||
|
|
||||||
|
(defn ex-info?
|
||||||
|
[v]
|
||||||
|
(instance? #?(:clj clojure.lang.ExceptionInfo :cljs cljs.core.ExceptionInfo) v))
|
||||||
|
|
|
@ -189,9 +189,7 @@
|
||||||
(let [edata (s/explain-data spec data)]
|
(let [edata (s/explain-data spec data)]
|
||||||
(throw (ex/error :type :validation
|
(throw (ex/error :type :validation
|
||||||
:code :spec-validation
|
:code :spec-validation
|
||||||
:explain (with-out-str
|
:data data))))
|
||||||
(expound/printer edata))
|
|
||||||
:data (::s/problems edata)))))
|
|
||||||
result))
|
result))
|
||||||
|
|
||||||
(defmacro instrument!
|
(defmacro instrument!
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue