mirror of
https://github.com/penpot/penpot.git
synced 2025-05-17 07:36:09 +02:00
Merge pull request #4863 from penpot/niwinz-refactor-backend-config
♻️ Refactor configuration validation
This commit is contained in:
commit
73fb95976c
14 changed files with 267 additions and 344 deletions
|
@ -9,7 +9,6 @@
|
||||||
[app.common.exceptions :as ex]
|
[app.common.exceptions :as ex]
|
||||||
[app.common.logging :as l]
|
[app.common.logging :as l]
|
||||||
[app.common.spec :as us]
|
[app.common.spec :as us]
|
||||||
[app.config :as cf]
|
|
||||||
[clj-ldap.client :as ldap]
|
[clj-ldap.client :as ldap]
|
||||||
[clojure.spec.alpha :as s]
|
[clojure.spec.alpha :as s]
|
||||||
[clojure.string]
|
[clojure.string]
|
||||||
|
@ -104,17 +103,17 @@
|
||||||
nil))))
|
nil))))
|
||||||
|
|
||||||
(s/def ::enabled? ::us/boolean)
|
(s/def ::enabled? ::us/boolean)
|
||||||
(s/def ::host ::cf/ldap-host)
|
(s/def ::host ::us/string)
|
||||||
(s/def ::port ::cf/ldap-port)
|
(s/def ::port ::us/integer)
|
||||||
(s/def ::ssl ::cf/ldap-ssl)
|
(s/def ::ssl ::us/boolean)
|
||||||
(s/def ::tls ::cf/ldap-starttls)
|
(s/def ::tls ::us/boolean)
|
||||||
(s/def ::query ::cf/ldap-user-query)
|
(s/def ::query ::us/string)
|
||||||
(s/def ::base-dn ::cf/ldap-base-dn)
|
(s/def ::base-dn ::us/string)
|
||||||
(s/def ::bind-dn ::cf/ldap-bind-dn)
|
(s/def ::bind-dn ::us/string)
|
||||||
(s/def ::bind-password ::cf/ldap-bind-password)
|
(s/def ::bind-password ::us/string)
|
||||||
(s/def ::attrs-email ::cf/ldap-attrs-email)
|
(s/def ::attrs-email ::us/string)
|
||||||
(s/def ::attrs-fullname ::cf/ldap-attrs-fullname)
|
(s/def ::attrs-fullname ::us/string)
|
||||||
(s/def ::attrs-username ::cf/ldap-attrs-username)
|
(s/def ::attrs-username ::us/string)
|
||||||
|
|
||||||
(s/def ::provider-params
|
(s/def ::provider-params
|
||||||
(s/keys :opt-un [::host ::port
|
(s/keys :opt-un [::host ::port
|
||||||
|
@ -126,6 +125,7 @@
|
||||||
::attrs-email
|
::attrs-email
|
||||||
::attrs-username
|
::attrs-username
|
||||||
::attrs-fullname]))
|
::attrs-fullname]))
|
||||||
|
|
||||||
(s/def ::provider
|
(s/def ::provider
|
||||||
(s/nilable ::provider-params))
|
(s/nilable ::provider-params))
|
||||||
|
|
||||||
|
|
|
@ -625,17 +625,17 @@
|
||||||
:provider provider
|
:provider provider
|
||||||
:hint "provider not configured"))))))})
|
:hint "provider not configured"))))))})
|
||||||
|
|
||||||
(s/def ::client-id ::cf/oidc-client-id)
|
(s/def ::client-id ::us/string)
|
||||||
(s/def ::client-secret ::cf/oidc-client-secret)
|
(s/def ::client-secret ::us/string)
|
||||||
(s/def ::base-uri ::cf/oidc-base-uri)
|
(s/def ::base-uri ::us/string)
|
||||||
(s/def ::token-uri ::cf/oidc-token-uri)
|
(s/def ::token-uri ::us/string)
|
||||||
(s/def ::auth-uri ::cf/oidc-auth-uri)
|
(s/def ::auth-uri ::us/string)
|
||||||
(s/def ::user-uri ::cf/oidc-user-uri)
|
(s/def ::user-uri ::us/string)
|
||||||
(s/def ::scopes ::cf/oidc-scopes)
|
(s/def ::scopes ::us/set-of-strings)
|
||||||
(s/def ::roles ::cf/oidc-roles)
|
(s/def ::roles ::us/set-of-strings)
|
||||||
(s/def ::roles-attr ::cf/oidc-roles-attr)
|
(s/def ::roles-attr ::us/string)
|
||||||
(s/def ::email-attr ::cf/oidc-email-attr)
|
(s/def ::email-attr ::us/string)
|
||||||
(s/def ::name-attr ::cf/oidc-name-attr)
|
(s/def ::name-attr ::us/string)
|
||||||
|
|
||||||
(s/def ::provider
|
(s/def ::provider
|
||||||
(s/keys :req-un [::client-id
|
(s/keys :req-un [::client-id
|
||||||
|
|
|
@ -11,30 +11,17 @@
|
||||||
[app.common.data :as d]
|
[app.common.data :as d]
|
||||||
[app.common.exceptions :as ex]
|
[app.common.exceptions :as ex]
|
||||||
[app.common.flags :as flags]
|
[app.common.flags :as flags]
|
||||||
[app.common.spec :as us]
|
[app.common.schema :as sm]
|
||||||
[app.common.version :as v]
|
[app.common.version :as v]
|
||||||
|
[app.util.overrides]
|
||||||
[app.util.time :as dt]
|
[app.util.time :as dt]
|
||||||
[clojure.core :as c]
|
[clojure.core :as c]
|
||||||
[clojure.java.io :as io]
|
[clojure.java.io :as io]
|
||||||
[clojure.pprint :as pprint]
|
|
||||||
[clojure.spec.alpha :as s]
|
|
||||||
[cuerdas.core :as str]
|
[cuerdas.core :as str]
|
||||||
[datoteka.fs :as fs]
|
[datoteka.fs :as fs]
|
||||||
[environ.core :refer [env]]
|
[environ.core :refer [env]]
|
||||||
[integrant.core :as ig]))
|
[integrant.core :as ig]))
|
||||||
|
|
||||||
(prefer-method print-method
|
|
||||||
clojure.lang.IRecord
|
|
||||||
clojure.lang.IDeref)
|
|
||||||
|
|
||||||
(prefer-method print-method
|
|
||||||
clojure.lang.IPersistentMap
|
|
||||||
clojure.lang.IDeref)
|
|
||||||
|
|
||||||
(prefer-method pprint/simple-dispatch
|
|
||||||
clojure.lang.IPersistentMap
|
|
||||||
clojure.lang.IDeref)
|
|
||||||
|
|
||||||
(defmethod ig/init-key :default
|
(defmethod ig/init-key :default
|
||||||
[_ data]
|
[_ data]
|
||||||
(d/without-nils data))
|
(d/without-nils data))
|
||||||
|
@ -45,15 +32,15 @@
|
||||||
(d/without-nils data)
|
(d/without-nils data)
|
||||||
data))
|
data))
|
||||||
|
|
||||||
(def defaults
|
(def default
|
||||||
{:database-uri "postgresql://postgres/penpot"
|
{:database-uri "postgresql://postgres/penpot"
|
||||||
:database-username "penpot"
|
:database-username "penpot"
|
||||||
:database-password "penpot"
|
:database-password "penpot"
|
||||||
|
|
||||||
:default-blob-version 5
|
:default-blob-version 4
|
||||||
|
|
||||||
:rpc-rlimit-config (fs/path "resources/rlimit.edn")
|
:rpc-rlimit-config "resources/rlimit.edn"
|
||||||
:rpc-climit-config (fs/path "resources/climit.edn")
|
:rpc-climit-config "resources/climit.edn"
|
||||||
|
|
||||||
:file-change-snapshot-every 5
|
:file-change-snapshot-every 5
|
||||||
:file-change-snapshot-timeout "3h"
|
:file-change-snapshot-timeout "3h"
|
||||||
|
@ -92,254 +79,142 @@
|
||||||
;; time to avoid email sending after profile modification
|
;; time to avoid email sending after profile modification
|
||||||
:email-verify-threshold "15m"})
|
:email-verify-threshold "15m"})
|
||||||
|
|
||||||
(s/def ::default-rpc-rlimit ::us/vector-of-strings)
|
(def schema:config
|
||||||
(s/def ::rpc-rlimit-config ::fs/path)
|
(do #_sm/optional-keys
|
||||||
(s/def ::rpc-climit-config ::fs/path)
|
[:map {:title "config"}
|
||||||
|
[:flags {:optional true} [::sm/set :string]]
|
||||||
|
[:admins {:optional true} [::sm/set ::sm/email]]
|
||||||
|
[:secret-key {:optional true} :string]
|
||||||
|
|
||||||
(s/def ::media-max-file-size ::us/integer)
|
[:tenant {:optional false} :string]
|
||||||
|
[:public-uri {:optional false} :string]
|
||||||
|
[:host {:optional false} :string]
|
||||||
|
|
||||||
(s/def ::flags ::us/vector-of-keywords)
|
[:http-server-port {:optional true} :int]
|
||||||
(s/def ::telemetry-enabled ::us/boolean)
|
[:http-server-host {:optional true} :string]
|
||||||
|
[:http-server-max-body-size {:optional true} :int]
|
||||||
|
[:http-server-max-multipart-body-size {:optional true} :int]
|
||||||
|
[:http-server-io-threads {:optional true} :int]
|
||||||
|
[:http-server-worker-threads {:optional true} :int]
|
||||||
|
|
||||||
(s/def ::audit-log-archive-uri ::us/string)
|
[:telemetry-uri {:optional true} :string]
|
||||||
(s/def ::audit-log-http-handler-concurrency ::us/integer)
|
[:telemetry-with-taiga {:optional true} :boolean] ;; DELETE
|
||||||
|
|
||||||
(s/def ::email-domain-blacklist ::fs/path)
|
[:file-change-snapshot-every {:optional true} :int]
|
||||||
(s/def ::email-domain-whitelist ::fs/path)
|
[:file-change-snapshot-timeout {:optional true} ::dt/duration]
|
||||||
|
|
||||||
(s/def ::deletion-delay ::dt/duration)
|
[:media-max-file-size {:optional true} :int]
|
||||||
|
[:deletion-delay {:optional true} ::dt/duration] ;; REVIEW
|
||||||
|
[:telemetry-enabled {:optional true} :boolean]
|
||||||
|
[:default-blob-version {:optional true} :int]
|
||||||
|
[:allow-demo-users {:optional true} :boolean]
|
||||||
|
[:error-report-webhook {:optional true} :string]
|
||||||
|
[:user-feedback-destination {:optional true} :string]
|
||||||
|
|
||||||
(s/def ::admins ::us/set-of-valid-emails)
|
[:default-rpc-rlimit {:optional true} [::sm/vec :string]]
|
||||||
(s/def ::file-change-snapshot-every ::us/integer)
|
[:rpc-rlimit-config {:optional true} ::fs/path]
|
||||||
(s/def ::file-change-snapshot-timeout ::dt/duration)
|
[:rpc-climit-config {:optional true} ::fs/path]
|
||||||
|
|
||||||
(s/def ::default-executor-parallelism ::us/integer)
|
[:audit-log-archive-uri {:optional true} :string]
|
||||||
(s/def ::scheduled-executor-parallelism ::us/integer)
|
[:audit-log-http-handler-concurrency {:optional true} :int]
|
||||||
|
|
||||||
(s/def ::worker-default-parallelism ::us/integer)
|
[:default-executor-parallelism {:optional true} :int] ;; REVIEW
|
||||||
(s/def ::worker-webhook-parallelism ::us/integer)
|
[:scheduled-executor-parallelism {:optional true} :int] ;; REVIEW
|
||||||
|
[:worker-default-parallelism {:optional true} :int]
|
||||||
|
[:worker-webhook-parallelism {:optional true} :int]
|
||||||
|
|
||||||
(s/def ::auth-data-cookie-domain ::us/string)
|
[:database-password {:optional true} [:maybe :string]]
|
||||||
(s/def ::auth-token-cookie-name ::us/string)
|
[:database-uri {:optional true} :string]
|
||||||
(s/def ::auth-token-cookie-max-age ::dt/duration)
|
[:database-username {:optional true} [:maybe :string]]
|
||||||
|
[:database-readonly {:optional true} :boolean]
|
||||||
|
[:database-min-pool-size {:optional true} :int]
|
||||||
|
[:database-max-pool-size {:optional true} :int]
|
||||||
|
|
||||||
(s/def ::secret-key ::us/string)
|
[:quotes-teams-per-profile {:optional true} :int]
|
||||||
(s/def ::allow-demo-users ::us/boolean)
|
[:quotes-access-tokens-per-profile {:optional true} :int]
|
||||||
(s/def ::assets-path ::us/string)
|
[:quotes-projects-per-team {:optional true} :int]
|
||||||
(s/def ::database-password (s/nilable ::us/string))
|
[:quotes-invitations-per-team {:optional true} :int]
|
||||||
(s/def ::database-uri ::us/string)
|
[:quotes-profiles-per-team {:optional true} :int]
|
||||||
(s/def ::database-username (s/nilable ::us/string))
|
[:quotes-files-per-project {:optional true} :int]
|
||||||
(s/def ::database-readonly ::us/boolean)
|
[:quotes-files-per-team {:optional true} :int]
|
||||||
(s/def ::database-min-pool-size ::us/integer)
|
[:quotes-font-variants-per-team {:optional true} :int]
|
||||||
(s/def ::database-max-pool-size ::us/integer)
|
[:quotes-comment-threads-per-file {:optional true} :int]
|
||||||
|
[:quotes-comments-per-file {:optional true} :int]
|
||||||
|
|
||||||
(s/def ::quotes-teams-per-profile ::us/integer)
|
[:auth-data-cookie-domain {:optional true} :string]
|
||||||
(s/def ::quotes-access-tokens-per-profile ::us/integer)
|
[:auth-token-cookie-name {:optional true} :string]
|
||||||
(s/def ::quotes-projects-per-team ::us/integer)
|
[:auth-token-cookie-max-age {:optional true} ::dt/duration]
|
||||||
(s/def ::quotes-invitations-per-team ::us/integer)
|
|
||||||
(s/def ::quotes-profiles-per-team ::us/integer)
|
|
||||||
(s/def ::quotes-files-per-project ::us/integer)
|
|
||||||
(s/def ::quotes-files-per-team ::us/integer)
|
|
||||||
(s/def ::quotes-font-variants-per-team ::us/integer)
|
|
||||||
(s/def ::quotes-comment-threads-per-file ::us/integer)
|
|
||||||
(s/def ::quotes-comments-per-file ::us/integer)
|
|
||||||
|
|
||||||
(s/def ::default-blob-version ::us/integer)
|
[:registration-domain-whitelist {:optional true} [::sm/set :string]]
|
||||||
(s/def ::error-report-webhook ::us/string)
|
[:email-verify-threshold {:optional true} ::dt/duration]
|
||||||
(s/def ::user-feedback-destination ::us/string)
|
|
||||||
(s/def ::github-client-id ::us/string)
|
|
||||||
(s/def ::github-client-secret ::us/string)
|
|
||||||
(s/def ::gitlab-base-uri ::us/string)
|
|
||||||
(s/def ::gitlab-client-id ::us/string)
|
|
||||||
(s/def ::gitlab-client-secret ::us/string)
|
|
||||||
(s/def ::google-client-id ::us/string)
|
|
||||||
(s/def ::google-client-secret ::us/string)
|
|
||||||
(s/def ::oidc-client-id ::us/string)
|
|
||||||
(s/def ::oidc-user-info-source ::us/keyword)
|
|
||||||
(s/def ::oidc-client-secret ::us/string)
|
|
||||||
(s/def ::oidc-base-uri ::us/string)
|
|
||||||
(s/def ::oidc-token-uri ::us/string)
|
|
||||||
(s/def ::oidc-auth-uri ::us/string)
|
|
||||||
(s/def ::oidc-user-uri ::us/string)
|
|
||||||
(s/def ::oidc-jwks-uri ::us/string)
|
|
||||||
(s/def ::oidc-scopes ::us/set-of-strings)
|
|
||||||
(s/def ::oidc-roles ::us/set-of-strings)
|
|
||||||
(s/def ::oidc-roles-attr ::us/string)
|
|
||||||
(s/def ::oidc-email-attr ::us/string)
|
|
||||||
(s/def ::oidc-name-attr ::us/string)
|
|
||||||
(s/def ::host ::us/string)
|
|
||||||
(s/def ::http-server-port ::us/integer)
|
|
||||||
(s/def ::http-server-host ::us/string)
|
|
||||||
(s/def ::http-server-max-body-size ::us/integer)
|
|
||||||
(s/def ::http-server-max-multipart-body-size ::us/integer)
|
|
||||||
(s/def ::http-server-io-threads ::us/integer)
|
|
||||||
(s/def ::http-server-worker-threads ::us/integer)
|
|
||||||
(s/def ::ldap-attrs-email ::us/string)
|
|
||||||
(s/def ::ldap-attrs-fullname ::us/string)
|
|
||||||
(s/def ::ldap-attrs-username ::us/string)
|
|
||||||
(s/def ::ldap-base-dn ::us/string)
|
|
||||||
(s/def ::ldap-bind-dn ::us/string)
|
|
||||||
(s/def ::ldap-bind-password ::us/string)
|
|
||||||
(s/def ::ldap-host ::us/string)
|
|
||||||
(s/def ::ldap-port ::us/integer)
|
|
||||||
(s/def ::ldap-ssl ::us/boolean)
|
|
||||||
(s/def ::ldap-starttls ::us/boolean)
|
|
||||||
(s/def ::ldap-user-query ::us/string)
|
|
||||||
(s/def ::media-directory ::us/string)
|
|
||||||
(s/def ::media-uri ::us/string)
|
|
||||||
(s/def ::profile-bounce-max-age ::dt/duration)
|
|
||||||
(s/def ::profile-bounce-threshold ::us/integer)
|
|
||||||
(s/def ::profile-complaint-max-age ::dt/duration)
|
|
||||||
(s/def ::profile-complaint-threshold ::us/integer)
|
|
||||||
(s/def ::public-uri ::us/string)
|
|
||||||
(s/def ::redis-uri ::us/string)
|
|
||||||
(s/def ::registration-domain-whitelist ::us/set-of-strings)
|
|
||||||
|
|
||||||
(s/def ::smtp-default-from ::us/string)
|
[:github-client-id {:optional true} :string]
|
||||||
(s/def ::smtp-default-reply-to ::us/string)
|
[:github-client-secret {:optional true} :string]
|
||||||
(s/def ::smtp-host ::us/string)
|
[:gitlab-base-uri {:optional true} :string]
|
||||||
(s/def ::smtp-password (s/nilable ::us/string))
|
[:gitlab-client-id {:optional true} :string]
|
||||||
(s/def ::smtp-port ::us/integer)
|
[:gitlab-client-secret {:optional true} :string]
|
||||||
(s/def ::smtp-ssl ::us/boolean)
|
[:google-client-id {:optional true} :string]
|
||||||
(s/def ::smtp-tls ::us/boolean)
|
[:google-client-secret {:optional true} :string]
|
||||||
(s/def ::smtp-username (s/nilable ::us/string))
|
[:oidc-client-id {:optional true} :string]
|
||||||
(s/def ::urepl-host ::us/string)
|
[:oidc-user-info-source {:optional true} :keyword]
|
||||||
(s/def ::urepl-port ::us/integer)
|
[:oidc-client-secret {:optional true} :string]
|
||||||
(s/def ::prepl-host ::us/string)
|
[:oidc-base-uri {:optional true} :string]
|
||||||
(s/def ::prepl-port ::us/integer)
|
[:oidc-token-uri {:optional true} :string]
|
||||||
(s/def ::assets-storage-backend ::us/keyword)
|
[:oidc-auth-uri {:optional true} :string]
|
||||||
(s/def ::storage-assets-fs-directory ::us/string)
|
[:oidc-user-uri {:optional true} :string]
|
||||||
(s/def ::storage-assets-s3-bucket ::us/string)
|
[:oidc-jwks-uri {:optional true} :string]
|
||||||
(s/def ::storage-assets-s3-region ::us/keyword)
|
[:oidc-scopes {:optional true} [::sm/set :string]]
|
||||||
(s/def ::storage-assets-s3-endpoint ::us/string)
|
[:oidc-roles {:optional true} [::sm/set :string]]
|
||||||
(s/def ::storage-assets-s3-io-threads ::us/integer)
|
[:oidc-roles-attr {:optional true} :string]
|
||||||
(s/def ::telemetry-uri ::us/string)
|
[:oidc-email-attr {:optional true} :string]
|
||||||
(s/def ::telemetry-with-taiga ::us/boolean)
|
[:oidc-name-attr {:optional true} :string]
|
||||||
(s/def ::tenant ::us/string)
|
|
||||||
(s/def ::email-verify-threshold ::dt/duration)
|
|
||||||
|
|
||||||
(s/def ::config
|
[:ldap-attrs-email {:optional true} :string]
|
||||||
(s/keys :opt-un [::secret-key
|
[:ldap-attrs-fullname {:optional true} :string]
|
||||||
::flags
|
[:ldap-attrs-username {:optional true} :string]
|
||||||
::admins
|
[:ldap-base-dn {:optional true} :string]
|
||||||
::deletion-delay
|
[:ldap-bind-dn {:optional true} :string]
|
||||||
::allow-demo-users
|
[:ldap-bind-password {:optional true} :string]
|
||||||
::audit-log-archive-uri
|
[:ldap-host {:optional true} :string]
|
||||||
::audit-log-http-handler-concurrency
|
[:ldap-port {:optional true} :int]
|
||||||
::auth-token-cookie-name
|
[:ldap-ssl {:optional true} :boolean]
|
||||||
::auth-token-cookie-max-age
|
[:ldap-starttls {:optional true} :boolean]
|
||||||
::authenticated-cookie-domain
|
[:ldap-user-query {:optional true} :string]
|
||||||
::database-password
|
|
||||||
::database-uri
|
|
||||||
::database-username
|
|
||||||
::database-readonly
|
|
||||||
::database-min-pool-size
|
|
||||||
::database-max-pool-size
|
|
||||||
::default-blob-version
|
|
||||||
::default-rpc-rlimit
|
|
||||||
::email-domain-blacklist
|
|
||||||
::email-domain-whitelist
|
|
||||||
::error-report-webhook
|
|
||||||
::default-executor-parallelism
|
|
||||||
::scheduled-executor-parallelism
|
|
||||||
::worker-default-parallelism
|
|
||||||
::worker-webhook-parallelism
|
|
||||||
::file-change-snapshot-every
|
|
||||||
::file-change-snapshot-timeout
|
|
||||||
::user-feedback-destination
|
|
||||||
::github-client-id
|
|
||||||
::github-client-secret
|
|
||||||
::gitlab-base-uri
|
|
||||||
::gitlab-client-id
|
|
||||||
::gitlab-client-secret
|
|
||||||
::google-client-id
|
|
||||||
::google-client-secret
|
|
||||||
::oidc-client-id
|
|
||||||
::oidc-client-secret
|
|
||||||
::oidc-user-info-source
|
|
||||||
::oidc-base-uri
|
|
||||||
::oidc-token-uri
|
|
||||||
::oidc-auth-uri
|
|
||||||
::oidc-user-uri
|
|
||||||
::oidc-jwks-uri
|
|
||||||
::oidc-scopes
|
|
||||||
::oidc-roles-attr
|
|
||||||
::oidc-email-attr
|
|
||||||
::oidc-name-attr
|
|
||||||
::oidc-roles
|
|
||||||
::host
|
|
||||||
::http-server-host
|
|
||||||
::http-server-port
|
|
||||||
::http-server-max-body-size
|
|
||||||
::http-server-max-multipart-body-size
|
|
||||||
::http-server-io-threads
|
|
||||||
::http-server-worker-threads
|
|
||||||
::ldap-attrs-email
|
|
||||||
::ldap-attrs-fullname
|
|
||||||
::ldap-attrs-username
|
|
||||||
::ldap-base-dn
|
|
||||||
::ldap-bind-dn
|
|
||||||
::ldap-bind-password
|
|
||||||
::ldap-host
|
|
||||||
::ldap-port
|
|
||||||
::ldap-ssl
|
|
||||||
::ldap-starttls
|
|
||||||
::ldap-user-query
|
|
||||||
::local-assets-uri
|
|
||||||
::media-max-file-size
|
|
||||||
::profile-bounce-max-age
|
|
||||||
::profile-bounce-threshold
|
|
||||||
::profile-complaint-max-age
|
|
||||||
::profile-complaint-threshold
|
|
||||||
::public-uri
|
|
||||||
|
|
||||||
::quotes-teams-per-profile
|
[:profile-bounce-max-age {:optional true} ::dt/duration]
|
||||||
::quotes-access-tokens-per-profile
|
[:profile-bounce-threshold {:optional true} :int]
|
||||||
::quotes-projects-per-team
|
[:profile-complaint-max-age {:optional true} ::dt/duration]
|
||||||
::quotes-invitations-per-team
|
[:profile-complaint-threshold {:optional true} :int]
|
||||||
::quotes-profiles-per-team
|
|
||||||
::quotes-files-per-project
|
|
||||||
::quotes-files-per-team
|
|
||||||
::quotes-font-variants-per-team
|
|
||||||
::quotes-comment-threads-per-file
|
|
||||||
::quotes-comments-per-file
|
|
||||||
|
|
||||||
::redis-uri
|
[:redis-uri {:optional true} :string]
|
||||||
::registration-domain-whitelist
|
|
||||||
::rpc-rlimit-config
|
|
||||||
::rpc-climit-config
|
|
||||||
|
|
||||||
::semaphore-process-font
|
[:email-domain-blacklist {:optional true} ::fs/path]
|
||||||
::semaphore-process-image
|
[:email-domain-whitelist {:optional true} ::fs/path]
|
||||||
::semaphore-update-file
|
|
||||||
::semaphore-auth
|
|
||||||
|
|
||||||
::smtp-default-from
|
[:smtp-default-from {:optional true} :string]
|
||||||
::smtp-default-reply-to
|
[:smtp-default-reply-to {:optional true} :string]
|
||||||
::smtp-host
|
[:smtp-host {:optional true} :string]
|
||||||
::smtp-password
|
[:smtp-password {:optional true} [:maybe :string]]
|
||||||
::smtp-port
|
[:smtp-port {:optional true} :int]
|
||||||
::smtp-ssl
|
[:smtp-ssl {:optional true} :boolean]
|
||||||
::smtp-tls
|
[:smtp-tls {:optional true} :boolean]
|
||||||
::smtp-username
|
[:smtp-username {:optional true} [:maybe :string]]
|
||||||
|
|
||||||
::urepl-host
|
[:urepl-host {:optional true} :string]
|
||||||
::urepl-port
|
[:urepl-port {:optional true} :int]
|
||||||
::prepl-host
|
[:prepl-host {:optional true} :string]
|
||||||
::prepl-port
|
[:prepl-port {:optional true} :int]
|
||||||
|
|
||||||
::assets-storage-backend
|
[:assets-storage-backend {:optional true} :keyword]
|
||||||
::storage-assets-fs-directory
|
[:media-directory {:optional true} :string] ;; REVIEW
|
||||||
::storage-assets-s3-bucket
|
[:media-uri {:optional true} :string]
|
||||||
::storage-assets-s3-region
|
[:assets-path {:optional true} :string]
|
||||||
::storage-assets-s3-endpoint
|
|
||||||
::storage-assets-s3-io-threads
|
[:storage-assets-fs-directory {:optional true} :string]
|
||||||
::telemetry-enabled
|
[:storage-assets-s3-bucket {:optional true} :string]
|
||||||
::telemetry-uri
|
[:storage-assets-s3-region {:optional true} :keyword]
|
||||||
::telemetry-referer
|
[:storage-assets-s3-endpoint {:optional true} :string]
|
||||||
::telemetry-with-taiga
|
[:storage-assets-s3-io-threads {:optional true} :int]]))
|
||||||
::tenant
|
|
||||||
::email-verify-threshold]))
|
|
||||||
|
|
||||||
(def default-flags
|
(def default-flags
|
||||||
[:enable-backend-api-doc
|
[:enable-backend-api-doc
|
||||||
|
@ -367,20 +242,22 @@
|
||||||
{}
|
{}
|
||||||
env)))
|
env)))
|
||||||
|
|
||||||
(defn- read-config
|
(def decode-config
|
||||||
[]
|
(sm/decoder schema:config sm/default-transformer))
|
||||||
(try
|
|
||||||
(->> (read-env "penpot")
|
(def validate-config
|
||||||
(merge defaults)
|
(sm/validator schema:config))
|
||||||
(us/conform ::config))
|
|
||||||
(catch Throwable e
|
(def explain-config
|
||||||
(when (ex/error? e)
|
(sm/explainer schema:config))
|
||||||
(println ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;")
|
|
||||||
(println "Error on validating configuration:")
|
(defn read-config
|
||||||
(println (some-> e ex-data ex/explain))
|
"Reads the configuration from enviroment variables and decodes all
|
||||||
(println (ex/explain (ex-data e)))
|
known values."
|
||||||
(println ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"))
|
[& {:keys [prefix default] :or {prefix "penpot"}}]
|
||||||
(throw e))))
|
(->> (read-env prefix)
|
||||||
|
(merge default)
|
||||||
|
(decode-config)))
|
||||||
|
|
||||||
(def version
|
(def version
|
||||||
(v/parse (or (some-> (io/resource "version.txt")
|
(v/parse (or (some-> (io/resource "version.txt")
|
||||||
|
@ -388,10 +265,28 @@
|
||||||
(str/trim))
|
(str/trim))
|
||||||
"%version%")))
|
"%version%")))
|
||||||
|
|
||||||
(defonce ^:dynamic config (read-config))
|
(defonce ^:dynamic config (read-config :default default))
|
||||||
(defonce ^:dynamic flags (parse-flags config))
|
(defonce ^:dynamic flags (parse-flags config))
|
||||||
|
|
||||||
(def deletion-delay
|
(defn validate!
|
||||||
|
"Validate the currently loaded configuration data."
|
||||||
|
[& {:keys [exit-on-error?] :or {exit-on-error? true}}]
|
||||||
|
(if (validate-config config)
|
||||||
|
true
|
||||||
|
(let [explain (explain-config config)]
|
||||||
|
(println "Error on validating configuration:")
|
||||||
|
(sm/pretty-explain explain
|
||||||
|
:variant ::sm/schemaless-explain
|
||||||
|
:message "Configuration Validation Error")
|
||||||
|
(flush)
|
||||||
|
(if exit-on-error?
|
||||||
|
(System/exit -1)
|
||||||
|
(ex/raise :type :validation
|
||||||
|
:code :config-validaton
|
||||||
|
::sm/explain explain)))))
|
||||||
|
|
||||||
|
(defn get-deletion-delay
|
||||||
|
[]
|
||||||
(or (c/get config :deletion-delay)
|
(or (c/get config :deletion-delay)
|
||||||
(dt/duration {:days 7})))
|
(dt/duration {:days 7})))
|
||||||
|
|
||||||
|
|
|
@ -7,9 +7,11 @@
|
||||||
(ns app.email
|
(ns app.email
|
||||||
"Main api for send emails."
|
"Main api for send emails."
|
||||||
(:require
|
(:require
|
||||||
|
[app.common.data.macros :as dm]
|
||||||
[app.common.exceptions :as ex]
|
[app.common.exceptions :as ex]
|
||||||
[app.common.logging :as l]
|
[app.common.logging :as l]
|
||||||
[app.common.pprint :as pp]
|
[app.common.pprint :as pp]
|
||||||
|
[app.common.schema :as sm]
|
||||||
[app.common.spec :as us]
|
[app.common.spec :as us]
|
||||||
[app.config :as cf]
|
[app.config :as cf]
|
||||||
[app.db :as db]
|
[app.db :as db]
|
||||||
|
@ -149,9 +151,27 @@
|
||||||
"mail.smtp.timeout" timeout
|
"mail.smtp.timeout" timeout
|
||||||
"mail.smtp.connectiontimeout" timeout}))
|
"mail.smtp.connectiontimeout" timeout}))
|
||||||
|
|
||||||
|
(def ^:private schema:smtp-config
|
||||||
|
[:map
|
||||||
|
[::username {:optional true} :string]
|
||||||
|
[::password {:optional true} :string]
|
||||||
|
[::tls {:optional true} :boolean]
|
||||||
|
[::ssl {:optional true} :boolean]
|
||||||
|
[::host {:optional true} :string]
|
||||||
|
[::port {:optional true} :int]
|
||||||
|
[::default-from {:optional true} :string]
|
||||||
|
[::default-reply-to {:optional true} :string]])
|
||||||
|
|
||||||
|
(def valid-smtp-config?
|
||||||
|
(sm/check-fn schema:smtp-config))
|
||||||
|
|
||||||
(defn- create-smtp-session
|
(defn- create-smtp-session
|
||||||
^Session
|
^Session
|
||||||
[cfg]
|
[cfg]
|
||||||
|
(dm/assert!
|
||||||
|
"expected valid smtp config"
|
||||||
|
(valid-smtp-config? cfg))
|
||||||
|
|
||||||
(let [props (opts->props cfg)]
|
(let [props (opts->props cfg)]
|
||||||
(Session/getInstance props)))
|
(Session/getInstance props)))
|
||||||
|
|
||||||
|
@ -273,32 +293,10 @@
|
||||||
;; SENDMAIL FN / TASK HANDLER
|
;; SENDMAIL FN / TASK HANDLER
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
|
||||||
(s/def ::username ::cf/smtp-username)
|
|
||||||
(s/def ::password ::cf/smtp-password)
|
|
||||||
(s/def ::tls ::cf/smtp-tls)
|
|
||||||
(s/def ::ssl ::cf/smtp-ssl)
|
|
||||||
(s/def ::host ::cf/smtp-host)
|
|
||||||
(s/def ::port ::cf/smtp-port)
|
|
||||||
(s/def ::default-reply-to ::cf/smtp-default-reply-to)
|
|
||||||
(s/def ::default-from ::cf/smtp-default-from)
|
|
||||||
|
|
||||||
(s/def ::smtp-config
|
|
||||||
(s/keys :opt [::username
|
|
||||||
::password
|
|
||||||
::tls
|
|
||||||
::ssl
|
|
||||||
::host
|
|
||||||
::port
|
|
||||||
::default-from
|
|
||||||
::default-reply-to]))
|
|
||||||
|
|
||||||
(declare send-to-logger!)
|
(declare send-to-logger!)
|
||||||
|
|
||||||
(s/def ::sendmail fn?)
|
(s/def ::sendmail fn?)
|
||||||
|
|
||||||
(defmethod ig/pre-init-spec ::sendmail [_]
|
|
||||||
(s/spec ::smtp-config))
|
|
||||||
|
|
||||||
(defmethod ig/init-key ::sendmail
|
(defmethod ig/init-key ::sendmail
|
||||||
[_ cfg]
|
[_ cfg]
|
||||||
(fn [params]
|
(fn [params]
|
||||||
|
|
|
@ -524,6 +524,7 @@
|
||||||
|
|
||||||
(defn start
|
(defn start
|
||||||
[]
|
[]
|
||||||
|
(cf/validate!)
|
||||||
(ig/load-namespaces (merge system-config worker-config))
|
(ig/load-namespaces (merge system-config worker-config))
|
||||||
(alter-var-root #'system (fn [sys]
|
(alter-var-root #'system (fn [sys]
|
||||||
(when sys (ig/halt! sys))
|
(when sys (ig/halt! sys))
|
||||||
|
|
|
@ -11,7 +11,6 @@
|
||||||
[app.common.exceptions :as ex]
|
[app.common.exceptions :as ex]
|
||||||
[app.common.media :as cm]
|
[app.common.media :as cm]
|
||||||
[app.common.schema :as sm]
|
[app.common.schema :as sm]
|
||||||
[app.common.schema.generators :as sg]
|
|
||||||
[app.common.schema.openapi :as-alias oapi]
|
[app.common.schema.openapi :as-alias oapi]
|
||||||
[app.common.spec :as us]
|
[app.common.spec :as us]
|
||||||
[app.common.svg :as csvg]
|
[app.common.svg :as csvg]
|
||||||
|
@ -47,18 +46,6 @@
|
||||||
(s/keys :req-un [::path]
|
(s/keys :req-un [::path]
|
||||||
:opt-un [::mtype]))
|
:opt-un [::mtype]))
|
||||||
|
|
||||||
(sm/register! ::fs/path
|
|
||||||
{:type ::fs/path
|
|
||||||
:pred fs/path?
|
|
||||||
:type-properties
|
|
||||||
{:title "path"
|
|
||||||
:description "filesystem path"
|
|
||||||
:error/message "expected a valid fs path instance"
|
|
||||||
:gen/gen (sg/generator :string)
|
|
||||||
::oapi/type "string"
|
|
||||||
::oapi/format "unix-path"
|
|
||||||
::oapi/decode fs/path}})
|
|
||||||
|
|
||||||
(sm/register! ::upload
|
(sm/register! ::upload
|
||||||
[:map {:title "Upload"}
|
[:map {:title "Upload"}
|
||||||
[:filename :string]
|
[:filename :string]
|
||||||
|
|
|
@ -45,7 +45,7 @@
|
||||||
params {:email email
|
params {:email email
|
||||||
:fullname fullname
|
:fullname fullname
|
||||||
:is-active true
|
:is-active true
|
||||||
:deleted-at (dt/in-future cf/deletion-delay)
|
:deleted-at (dt/in-future (cf/get-deletion-delay))
|
||||||
:password (profile/derive-password cfg password)
|
:password (profile/derive-password cfg password)
|
||||||
:props {}}]
|
:props {}}]
|
||||||
|
|
||||||
|
|
|
@ -295,7 +295,7 @@
|
||||||
|
|
||||||
(defmethod ig/prep-key ::handler
|
(defmethod ig/prep-key ::handler
|
||||||
[_ cfg]
|
[_ cfg]
|
||||||
(assoc cfg ::min-age cf/deletion-delay))
|
(assoc cfg ::min-age (cf/get-deletion-delay)))
|
||||||
|
|
||||||
(defmethod ig/init-key ::handler
|
(defmethod ig/init-key ::handler
|
||||||
[_ cfg]
|
[_ cfg]
|
||||||
|
|
|
@ -292,7 +292,7 @@
|
||||||
(defmethod ig/prep-key ::handler
|
(defmethod ig/prep-key ::handler
|
||||||
[_ cfg]
|
[_ cfg]
|
||||||
(assoc cfg
|
(assoc cfg
|
||||||
::min-age cf/deletion-delay
|
::min-age (cf/get-deletion-delay)
|
||||||
::chunk-size 10))
|
::chunk-size 10))
|
||||||
|
|
||||||
(defmethod ig/init-key ::handler
|
(defmethod ig/init-key ::handler
|
||||||
|
|
|
@ -23,7 +23,7 @@
|
||||||
|
|
||||||
(defmethod ig/prep-key ::handler
|
(defmethod ig/prep-key ::handler
|
||||||
[_ cfg]
|
[_ cfg]
|
||||||
(assoc cfg ::min-age cf/deletion-delay))
|
(assoc cfg ::min-age (cf/get-deletion-delay)))
|
||||||
|
|
||||||
(defmethod ig/init-key ::handler
|
(defmethod ig/init-key ::handler
|
||||||
[_ {:keys [::db/pool ::min-age] :as cfg}]
|
[_ {:keys [::db/pool ::min-age] :as cfg}]
|
||||||
|
|
41
backend/src/app/util/overrides.clj
Normal file
41
backend/src/app/util/overrides.clj
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
;; 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/.
|
||||||
|
;;
|
||||||
|
;; Copyright (c) KALEIDOS INC
|
||||||
|
|
||||||
|
(ns app.util.overrides
|
||||||
|
"A utility ns for declare default overrides over clojure runtime"
|
||||||
|
(:require
|
||||||
|
[app.common.schema :as sm]
|
||||||
|
[app.common.schema.generators :as sg]
|
||||||
|
[app.common.schema.openapi :as-alias oapi]
|
||||||
|
[clojure.pprint :as pprint]
|
||||||
|
[datoteka.fs :as fs]))
|
||||||
|
|
||||||
|
|
||||||
|
(prefer-method print-method
|
||||||
|
clojure.lang.IRecord
|
||||||
|
clojure.lang.IDeref)
|
||||||
|
|
||||||
|
(prefer-method print-method
|
||||||
|
clojure.lang.IPersistentMap
|
||||||
|
clojure.lang.IDeref)
|
||||||
|
|
||||||
|
(prefer-method pprint/simple-dispatch
|
||||||
|
clojure.lang.IPersistentMap
|
||||||
|
clojure.lang.IDeref)
|
||||||
|
|
||||||
|
|
||||||
|
(sm/register! ::fs/path
|
||||||
|
{:type ::fs/path
|
||||||
|
:pred fs/path?
|
||||||
|
:type-properties
|
||||||
|
{:title "path"
|
||||||
|
:description "filesystem path"
|
||||||
|
:error/message "expected a valid fs path instance"
|
||||||
|
:error/code "errors.invalid-path"
|
||||||
|
:gen/gen (sg/generator :string)
|
||||||
|
::oapi/type "string"
|
||||||
|
::oapi/format "unix-path"
|
||||||
|
::oapi/decode fs/path}})
|
|
@ -58,15 +58,14 @@
|
||||||
(def ^:dynamic *system* nil)
|
(def ^:dynamic *system* nil)
|
||||||
(def ^:dynamic *pool* nil)
|
(def ^:dynamic *pool* nil)
|
||||||
|
|
||||||
(def defaults
|
(def default
|
||||||
{:database-uri "postgresql://postgres/penpot_test"
|
{:database-uri "postgresql://postgres/penpot_test"
|
||||||
:redis-uri "redis://redis/1"
|
:redis-uri "redis://redis/1"
|
||||||
:file-change-snapshot-every 1})
|
:file-change-snapshot-every 1})
|
||||||
|
|
||||||
(def config
|
(def config
|
||||||
(->> (cf/read-env "penpot-test")
|
(cf/read-config :prefix "penpot-test"
|
||||||
(merge cf/defaults defaults)
|
:default (merge cf/default default)))
|
||||||
(us/conform ::cf/config)))
|
|
||||||
|
|
||||||
(def default-flags
|
(def default-flags
|
||||||
[:enable-secure-session-cookies
|
[:enable-secure-session-cookies
|
||||||
|
@ -88,6 +87,8 @@
|
||||||
app.auth/verify-password (fn [a b] {:valid (= a b)})
|
app.auth/verify-password (fn [a b] {:valid (= a b)})
|
||||||
app.common.features/get-enabled-features (fn [& _] app.common.features/supported-features)]
|
app.common.features/get-enabled-features (fn [& _] app.common.features/supported-features)]
|
||||||
|
|
||||||
|
(cf/validate! :exit-on-error? false)
|
||||||
|
|
||||||
(fs/create-dir "/tmp/penpot")
|
(fs/create-dir "/tmp/penpot")
|
||||||
|
|
||||||
(let [templates [{:id "test"
|
(let [templates [{:id "test"
|
||||||
|
@ -524,7 +525,6 @@
|
||||||
([key default]
|
([key default]
|
||||||
(get data key (get cf/config key default)))))
|
(get data key (get cf/config key default)))))
|
||||||
|
|
||||||
|
|
||||||
(defn reset-mock!
|
(defn reset-mock!
|
||||||
[m]
|
[m]
|
||||||
(swap! m (fn [m]
|
(swap! m (fn [m]
|
||||||
|
|
|
@ -1127,9 +1127,9 @@
|
||||||
(t/is (= 1 (:processed res))))
|
(t/is (= 1 (:processed res))))
|
||||||
|
|
||||||
;; check that object thumbnails are still here
|
;; check that object thumbnails are still here
|
||||||
(let [res (th/db-exec! ["select * from file_tagged_object_thumbnail"])]
|
(let [rows (th/db-query :file-tagged-object-thumbnail {:file-id (:id file)})]
|
||||||
;; (th/print-result! res)
|
;; (app.common.pprint/pprint rows)
|
||||||
(t/is (= 1 (count res))))
|
(t/is (= 1 (count rows))))
|
||||||
|
|
||||||
;; insert object snapshot for for unknown frame
|
;; insert object snapshot for for unknown frame
|
||||||
(let [data {::th/type :create-file-object-thumbnail
|
(let [data {::th/type :create-file-object-thumbnail
|
||||||
|
@ -1148,13 +1148,20 @@
|
||||||
(th/db-exec! ["update file set has_media_trimmed=false where id=?" (:id file)])
|
(th/db-exec! ["update file set has_media_trimmed=false where id=?" (:id file)])
|
||||||
|
|
||||||
;; check that we have all object thumbnails
|
;; check that we have all object thumbnails
|
||||||
(let [res (th/db-exec! ["select * from file_tagged_object_thumbnail"])]
|
(let [rows (th/db-query :file-tagged-object-thumbnail {:file-id (:id file)})]
|
||||||
(t/is (= 2 (count res))))
|
;; (app.common.pprint/pprint rows)
|
||||||
|
(t/is (= 2 (count rows))))
|
||||||
|
|
||||||
;; run the task again
|
;; run the task again
|
||||||
(let [res (th/run-task! :file-gc {:min-age 0})]
|
(let [res (th/run-task! :file-gc {:min-age 0})]
|
||||||
(t/is (= 1 (:processed res))))
|
(t/is (= 1 (:processed res))))
|
||||||
|
|
||||||
|
;; check that we have all object thumbnails
|
||||||
|
(let [rows (th/db-query :file-tagged-object-thumbnail {:file-id (:id file)})]
|
||||||
|
;; (app.common.pprint/pprint rows)
|
||||||
|
(t/is (= 2 (count rows))))
|
||||||
|
|
||||||
|
|
||||||
;; check that the unknown frame thumbnail is deleted
|
;; check that the unknown frame thumbnail is deleted
|
||||||
(let [rows (th/db-query :file-tagged-object-thumbnail {:file-id (:id file)})]
|
(let [rows (th/db-query :file-tagged-object-thumbnail {:file-id (:id file)})]
|
||||||
(t/is (= 2 (count rows)))
|
(t/is (= 2 (count rows)))
|
||||||
|
@ -1164,6 +1171,7 @@
|
||||||
(t/is (= 3 (:processed res))))
|
(t/is (= 3 (:processed res))))
|
||||||
|
|
||||||
(let [rows (th/db-query :file-tagged-object-thumbnail {:file-id (:id file)})]
|
(let [rows (th/db-query :file-tagged-object-thumbnail {:file-id (:id file)})]
|
||||||
|
;; (app.common.pprint/pprint rows)
|
||||||
(t/is (= 1 (count rows)))))))
|
(t/is (= 1 (count rows)))))))
|
||||||
|
|
||||||
(t/deftest file-thumbnail-ops
|
(t/deftest file-thumbnail-ops
|
||||||
|
@ -1220,7 +1228,3 @@
|
||||||
|
|
||||||
(let [rows (th/db-query :file-thumbnail {:file-id (:id file)})]
|
(let [rows (th/db-query :file-thumbnail {:file-id (:id file)})]
|
||||||
(t/is (= 1 (count rows)))))))
|
(t/is (= 1 (count rows)))))))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -219,12 +219,10 @@
|
||||||
:length (d/nilv length 12)})))))
|
:length (d/nilv length 12)})))))
|
||||||
|
|
||||||
(defmethod v/-format ::schemaless-explain
|
(defmethod v/-format ::schemaless-explain
|
||||||
[_ {:keys [schema] :as explanation} printer]
|
[_ explanation printer]
|
||||||
{:body [:group
|
{:body [:group
|
||||||
(v/-block "Value" (v/-visit (me/error-value explanation printer) printer) printer) :break :break
|
(v/-block "Value" (v/-visit (me/error-value explanation printer) printer) printer) :break :break
|
||||||
(v/-block "Errors" (v/-visit (me/humanize (me/with-spell-checking explanation)) printer) printer) :break :break
|
(v/-block "Errors" (v/-visit (me/humanize (me/with-spell-checking explanation)) printer) printer)]})
|
||||||
(v/-block "Schema" (v/-visit schema printer) printer)]})
|
|
||||||
|
|
||||||
|
|
||||||
(defmethod v/-format ::explain
|
(defmethod v/-format ::explain
|
||||||
[_ {:keys [schema] :as explanation} printer]
|
[_ {:keys [schema] :as explanation} printer]
|
||||||
|
@ -233,7 +231,6 @@
|
||||||
(v/-block "Errors" (v/-visit (me/humanize (me/with-spell-checking explanation)) printer) printer) :break :break
|
(v/-block "Errors" (v/-visit (me/humanize (me/with-spell-checking explanation)) printer) printer) :break :break
|
||||||
(v/-block "Schema" (v/-visit schema printer) printer)]})
|
(v/-block "Schema" (v/-visit schema printer) printer)]})
|
||||||
|
|
||||||
|
|
||||||
(defn pretty-explain
|
(defn pretty-explain
|
||||||
[explain & {:keys [variant message]
|
[explain & {:keys [variant message]
|
||||||
:or {variant ::explain
|
:or {variant ::explain
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue