♻️ Add better ergonomics for the internal quotes API

This commit is contained in:
Andrey Antukh 2024-10-03 15:56:53 +02:00 committed by Alonso Torres
parent 3e11b4aa74
commit a1f5bcae80
13 changed files with 412 additions and 346 deletions

View file

@ -23,6 +23,7 @@ export PENPOT_FLAGS="\
enable-urepl-server \ enable-urepl-server \
enable-rpc-climit \ enable-rpc-climit \
enable-rpc-rlimit \ enable-rpc-rlimit \
enable-quotes \
enable-soft-rpc-rlimit \ enable-soft-rpc-rlimit \
enable-auto-file-snapshot \ enable-auto-file-snapshot \
enable-webhooks \ enable-webhooks \

View file

@ -17,6 +17,7 @@ export PENPOT_FLAGS="\
disable-secure-session-cookies \ disable-secure-session-cookies \
enable-rpc-climit \ enable-rpc-climit \
enable-smtp \ enable-smtp \
enable-quotes \
enable-file-snapshot \ enable-file-snapshot \
enable-access-tokens \ enable-access-tokens \
enable-tiered-file-data-storage \ enable-tiered-file-data-storage \

View file

@ -30,9 +30,8 @@
:tid token-id :tid token-id
:iat created-at}) :iat created-at})
expires-at (some-> expiration dt/in-future)] expires-at (some-> expiration dt/in-future)
token (db/insert! conn :access-token
(db/insert! conn :access-token
{:id token-id {:id token-id
:name name :name name
:token token :token token
@ -40,8 +39,8 @@
:created-at created-at :created-at created-at
:updated-at created-at :updated-at created-at
:expires-at expires-at :expires-at expires-at
:perms (db/create-array conn "text" [])}))) :perms (db/create-array conn "text" [])})]
(decode-row token)))
(defn repl:create-access-token (defn repl:create-access-token
[{:keys [::db/pool] :as system} profile-id name expiration] [{:keys [::db/pool] :as system} profile-id name expiration]
@ -60,14 +59,12 @@
(sv/defmethod ::create-access-token (sv/defmethod ::create-access-token
{::doc/added "1.18" {::doc/added "1.18"
::sm/params schema:create-access-token} ::sm/params schema:create-access-token}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id name expiration]}] [cfg {:keys [::rpc/profile-id name expiration]}]
(db/with-atomic [conn pool]
(let [cfg (assoc cfg ::db/conn conn)] (quotes/check! cfg {::quotes/id ::quotes/access-tokens-per-profile
(quotes/check-quote! conn
{::quotes/id ::quotes/access-tokens-per-profile
::quotes/profile-id profile-id}) ::quotes/profile-id profile-id})
(-> (create-access-token cfg profile-id name expiration)
(decode-row))))) (db/tx-run! cfg create-access-token profile-id name expiration))
(def ^:private schema:delete-access-token (def ^:private schema:delete-access-token
[:map {:title "delete-access-token"} [:map {:title "delete-access-token"}

View file

@ -297,29 +297,29 @@
[:frame-id ::sm/uuid] [:frame-id ::sm/uuid]
[:share-id {:optional true} [:maybe ::sm/uuid]]]) [:share-id {:optional true} [:maybe ::sm/uuid]]])
;; FIXME: relax transaction requirements
(sv/defmethod ::create-comment-thread (sv/defmethod ::create-comment-thread
{::doc/added "1.15" {::doc/added "1.15"
::webhooks/event? true ::webhooks/event? true
::rtry/enabled true ::rtry/enabled true
::rtry/when rtry/conflict-exception? ::rtry/when rtry/conflict-exception?
::sm/params schema:create-comment-thread} ::sm/params schema:create-comment-thread
[cfg {:keys [::rpc/profile-id ::rpc/request-at file-id page-id share-id position content frame-id]}] ::db/transaction true}
[{:keys [::db/conn] :as cfg}
{:keys [::rpc/profile-id ::rpc/request-at file-id page-id share-id position content frame-id]}]
(db/tx-run! cfg (fn [{:keys [::db/conn] :as cfg}]
(files/check-comment-permissions! cfg profile-id file-id share-id) (files/check-comment-permissions! cfg profile-id file-id share-id)
(let [{:keys [team-id project-id page-name]} (get-file conn file-id page-id)] (let [{:keys [team-id project-id page-name]} (get-file conn file-id page-id)]
(run! (partial quotes/check-quote! cfg) (-> cfg
(list {::quotes/id ::quotes/comment-threads-per-file (assoc ::quotes/profile-id profile-id)
::quotes/profile-id profile-id (assoc ::quotes/team-id team-id)
::quotes/team-id team-id (assoc ::quotes/project-id project-id)
::quotes/project-id project-id (assoc ::quotes/file-id file-id)
::quotes/file-id file-id} (quotes/check! {::quotes/id ::quotes/comment-threads-per-file}
{::quotes/id ::quotes/comments-per-file {::quotes/id ::quotes/comments-per-file}))
::quotes/profile-id profile-id
::quotes/team-id team-id
::quotes/project-id project-id
::quotes/file-id file-id}))
(create-comment-thread conn {:created-at request-at (create-comment-thread conn {:created-at request-at
:profile-id profile-id :profile-id profile-id
@ -328,7 +328,7 @@
:page-name page-name :page-name page-name
:position position :position position
:content content :content content
:frame-id frame-id}))))) :frame-id frame-id})))
(defn- create-comment-thread (defn- create-comment-thread
[conn {:keys [profile-id file-id page-id page-name created-at position content frame-id]}] [conn {:keys [profile-id file-id page-id page-name created-at position content frame-id]}]
@ -432,8 +432,7 @@
{:keys [team-id project-id page-name] :as file} (get-file cfg file-id page-id)] {:keys [team-id project-id page-name] :as file} (get-file cfg file-id page-id)]
(files/check-comment-permissions! conn profile-id file-id share-id) (files/check-comment-permissions! conn profile-id file-id share-id)
(quotes/check-quote! conn (quotes/check! cfg {::quotes/id ::quotes/comments-per-file
{::quotes/id ::quotes/comments-per-file
::quotes/profile-id profile-id ::quotes/profile-id profile-id
::quotes/team-id team-id ::quotes/team-id team-id
::quotes/project-id project-id ::quotes/project-id project-id

View file

@ -98,10 +98,9 @@
{::doc/added "1.17" {::doc/added "1.17"
::doc/module :files ::doc/module :files
::webhooks/event? true ::webhooks/event? true
::sm/params schema:create-file} ::sm/params schema:create-file
[cfg {:keys [::rpc/profile-id project-id] :as params}] ::db/transaction true}
(db/tx-run! cfg [{:keys [::db/conn] :as cfg} {:keys [::rpc/profile-id project-id] :as params}]
(fn [{:keys [::db/conn] :as cfg}]
(projects/check-edition-permissions! conn profile-id project-id) (projects/check-edition-permissions! conn profile-id project-id)
(let [team (teams/get-team conn (let [team (teams/get-team conn
:profile-id profile-id :profile-id profile-id
@ -125,11 +124,15 @@
(assoc :profile-id profile-id) (assoc :profile-id profile-id)
(assoc :features features))] (assoc :features features))]
(run! (partial quotes/check-quote! conn) (quotes/check! cfg {::quotes/id ::quotes/files-per-project
(list {::quotes/id ::quotes/files-per-project
::quotes/team-id team-id ::quotes/team-id team-id
::quotes/profile-id profile-id ::quotes/profile-id profile-id
::quotes/project-id project-id})) ::quotes/project-id project-id})
;; FIXME: IMPORTANT: this code can have race
;; conditions, because we have no locks for updating
;; team so, creating two files concurrently can lead
;; to lost team features updating
;; When newly computed features does not match exactly with ;; When newly computed features does not match exactly with
;; the features defined on team row, we update it. ;; the features defined on team row, we update it.
@ -140,4 +143,4 @@
{:id team-id}))) {:id team-id})))
(-> (create-file cfg params) (-> (create-file cfg params)
(vary-meta assoc ::audit/props {:team-id team-id})))))) (vary-meta assoc ::audit/props {:team-id team-id}))))

View file

@ -86,6 +86,9 @@
[:font-weight [::sm/one-of {:format "number"} valid-weight]] [:font-weight [::sm/one-of {:format "number"} valid-weight]]
[:font-style [::sm/one-of {:format "string"} valid-style]]]) [:font-style [::sm/one-of {:format "string"} valid-style]]])
;; FIXME: IMPORTANT: refactor this, we should not hold a whole db
;; connection around the font creation
(sv/defmethod ::create-font-variant (sv/defmethod ::create-font-variant
{::doc/added "1.18" {::doc/added "1.18"
::climit/id [[:process-font/by-profile ::rpc/profile-id] ::climit/id [[:process-font/by-profile ::rpc/profile-id]
@ -96,7 +99,7 @@
(db/tx-run! cfg (db/tx-run! cfg
(fn [{:keys [::db/conn] :as cfg}] (fn [{:keys [::db/conn] :as cfg}]
(teams/check-edition-permissions! conn profile-id team-id) (teams/check-edition-permissions! conn profile-id team-id)
(quotes/check-quote! conn {::quotes/id ::quotes/font-variants-per-team (quotes/check! cfg {::quotes/id ::quotes/font-variants-per-team
::quotes/profile-id profile-id ::quotes/profile-id profile-id
::quotes/team-id team-id}) ::quotes/team-id team-id})
(create-font-variant cfg (assoc params :profile-id profile-id))))) (create-font-variant cfg (assoc params :profile-id profile-id)))))

View file

@ -168,6 +168,17 @@
;; --- MUTATION: Create Project ;; --- MUTATION: Create Project
(defn- create-project
[{:keys [::db/conn] :as cfg} {:keys [profile-id team-id] :as params}]
(let [project (teams/create-project conn params)]
(teams/create-project-role conn profile-id (:id project) :owner)
(db/insert! conn :team-project-profile-rel
{:project-id (:id project)
:profile-id profile-id
:team-id team-id
:is-pinned false})
(assoc project :is-pinned false)))
(def ^:private schema:create-project (def ^:private schema:create-project
[:map {:title "create-project"} [:map {:title "create-project"}
[:team-id ::sm/uuid] [:team-id ::sm/uuid]
@ -178,23 +189,15 @@
{::doc/added "1.18" {::doc/added "1.18"
::webhooks/event? true ::webhooks/event? true
::sm/params schema:create-project} ::sm/params schema:create-project}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id] :as params}] [cfg {:keys [::rpc/profile-id team-id] :as params}]
(db/with-atomic [conn pool]
(teams/check-edition-permissions! conn profile-id team-id) (teams/check-edition-permissions! cfg profile-id team-id)
(quotes/check-quote! conn {::quotes/id ::quotes/projects-per-team (quotes/check! cfg {::quotes/id ::quotes/projects-per-team
::quotes/profile-id profile-id ::quotes/profile-id profile-id
::quotes/team-id team-id}) ::quotes/team-id team-id})
(let [params (assoc params :profile-id profile-id) (let [params (assoc params :profile-id profile-id)]
project (teams/create-project conn params)] (db/tx-run! cfg create-project params)))
(teams/create-project-role conn profile-id (:id project) :owner)
(db/insert! conn :team-project-profile-rel
{:project-id (:id project)
:profile-id profile-id
:team-id team-id
:is-pinned false})
(assoc project :is-pinned false))))
;; --- MUTATION: Toggle Project Pin ;; --- MUTATION: Toggle Project Pin

View file

@ -401,19 +401,22 @@
(sv/defmethod ::create-team (sv/defmethod ::create-team
{::doc/added "1.17" {::doc/added "1.17"
::sm/params schema:create-team} ::sm/params schema:create-team
::db/transaction true}
[cfg {:keys [::rpc/profile-id] :as params}] [cfg {:keys [::rpc/profile-id] :as params}]
(db/tx-run! cfg (fn [{:keys [::db/conn] :as cfg}]
(quotes/check-quote! conn {::quotes/id ::quotes/teams-per-profile (quotes/check! cfg {::quotes/id ::quotes/teams-per-profile
::quotes/profile-id profile-id}) ::quotes/profile-id profile-id})
(let [features (-> (cfeat/get-enabled-features cf/flags) (let [features (-> (cfeat/get-enabled-features cf/flags)
(cfeat/check-client-features! (:features params))) (cfeat/check-client-features! (:features params)))
team (create-team cfg (assoc params params (-> params
:profile-id profile-id (assoc :profile-id profile-id)
:features features))] (assoc :features features))
team (create-team cfg params)]
(with-meta team (with-meta team
{::audit/props {:id (:id team)}}))))) {::audit/props {:id (:id team)}})))
(defn create-team (defn create-team
"This is a complete team creation process, it creates the team "This is a complete team creation process, it creates the team
@ -867,8 +870,9 @@
(ex/raise :type :restriction (ex/raise :type :restriction
:code :profile-blocked)) :code :profile-blocked))
(quotes/check-quote! conn (quotes/check!
{::quotes/id ::quotes/profiles-per-team {::db/conn conn
::quotes/id ::quotes/profiles-per-team
::quotes/profile-id (:id member) ::quotes/profile-id (:id member)
::quotes/team-id team-id}) ::quotes/team-id team-id})
@ -916,38 +920,34 @@
"A rpc call that allow to send a single or multiple invitations to "A rpc call that allow to send a single or multiple invitations to
join the team." join the team."
{::doc/added "1.17" {::doc/added "1.17"
::sm/params schema:create-team-invitations} ::sm/params schema:create-team-invitations
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id emails role] :as params}] ::db/transaction true}
(db/with-atomic [conn pool] [{:keys [::db/conn] :as cfg} {:keys [::rpc/profile-id team-id emails role] :as params}]
(let [perms (get-permissions conn profile-id team-id) (let [perms (get-permissions conn profile-id team-id)
profile (db/get-by-id conn :profile profile-id) profile (db/get-by-id conn :profile profile-id)
team (db/get-by-id conn :team team-id) team (db/get-by-id conn :team team-id)
emails (into #{} (map profile/clean-email) emails)] emails (into #{} (map profile/clean-email) emails)]
(when-not (:is-admin perms)
(ex/raise :type :validation
:code :insufficient-permissions))
(when (> (count emails) max-invitations-by-request-threshold) (when (> (count emails) max-invitations-by-request-threshold)
(ex/raise :type :validation (ex/raise :type :validation
:code :max-invitations-by-request :code :max-invitations-by-request
:hint "the maximum of invitation on single request is reached" :hint "the maximum of invitation on single request is reached"
:threshold max-invitations-by-request-threshold)) :threshold max-invitations-by-request-threshold))
(run! (partial quotes/check-quote! conn) (-> cfg
(list {::quotes/id ::quotes/invitations-per-team (assoc ::quotes/profile-id profile-id)
::quotes/profile-id profile-id (assoc ::quotes/team-id team-id)
::quotes/team-id (:id team) (assoc ::quotes/incr (count emails))
::quotes/incr (count emails)} (quotes/check! {::quotes/id ::quotes/invitations-per-team}
{::quotes/id ::quotes/profiles-per-team {::quotes/id ::quotes/profiles-per-team}))
::quotes/profile-id profile-id
::quotes/team-id (:id team)
::quotes/incr (count emails)}))
(when-not (:is-admin perms) ;; Check if the current profile is allowed to send emails
(ex/raise :type :validation
:code :insufficient-permissions))
;; Check if the current profile is allowed to send emails.
(check-valid-email-muted conn profile) (check-valid-email-muted conn profile)
(let [requested (into #{} (map :email) (get-valid-requests-email conn team-id)) (let [requested (into #{} (map :email) (get-valid-requests-email conn team-id))
emails-to-add (filter #(contains? requested %) emails) emails-to-add (filter #(contains? requested %) emails)
emails (remove #(contains? requested %) emails) emails (remove #(contains? requested %) emails)
@ -967,13 +967,15 @@
(assoc :role role)))) (assoc :role role))))
(keep (partial create-invitation cfg))) (keep (partial create-invitation cfg)))
emails)] emails)]
;; For requested invitations, do not send invitation emails, add the user directly to the team ;; For requested invitations, do not send
;; invitation emails, add the user directly to
;; the team
(doseq [email emails-to-add] (doseq [email emails-to-add]
(add-user-to-team conn profile team email role)) (add-user-to-team conn profile team email role))
(with-meta {:total (count invitations) (with-meta {:total (count invitations)
:invitations invitations} :invitations invitations}
{::audit/props {:invitations (count invitations)}}))))) {::audit/props {:invitations (count invitations)}}))))
;; --- Mutation: Create Team & Invite Members ;; --- Mutation: Create Team & Invite Members
@ -987,11 +989,9 @@
(sv/defmethod ::create-team-with-invitations (sv/defmethod ::create-team-with-invitations
{::doc/added "1.17" {::doc/added "1.17"
::sm/params schema:create-team-with-invitations} ::sm/params schema:create-team-with-invitations
[cfg {:keys [::rpc/profile-id emails role name] :as params}] ::db/transaction true}
[{:keys [::db/conn] :as cfg} {:keys [::rpc/profile-id emails role name] :as params}]
(db/tx-run! cfg
(fn [{:keys [::db/conn] :as cfg}]
(let [features (-> (cfeat/get-enabled-features cf/flags) (let [features (-> (cfeat/get-enabled-features cf/flags)
(cfeat/check-client-features! (:features params))) (cfeat/check-client-features! (:features params)))
@ -999,11 +999,17 @@
(assoc :profile-id profile-id) (assoc :profile-id profile-id)
(assoc :features features)) (assoc :features features))
cfg (assoc cfg ::db/conn conn)
team (create-team cfg params) team (create-team cfg params)
profile (db/get-by-id conn :profile profile-id)
emails (into #{} (map profile/clean-email) emails)] emails (into #{} (map profile/clean-email) emails)]
(-> cfg
(assoc ::quotes/profile-id profile-id)
(assoc ::quotes/team-id (:id team))
(assoc ::quotes/incr (count emails))
(quotes/check! {::quotes/id ::quotes/teams-per-profile}
{::quotes/id ::quotes/invitations-per-team}
{::quotes/id ::quotes/profiles-per-team}))
(when (> (count emails) max-invitations-by-request-threshold) (when (> (count emails) max-invitations-by-request-threshold)
(ex/raise :type :validation (ex/raise :type :validation
:code :max-invitations-by-request :code :max-invitations-by-request
@ -1017,6 +1023,7 @@
(audit/submit! cfg event)) (audit/submit! cfg event))
;; Create invitations for all provided emails. ;; Create invitations for all provided emails.
(let [profile (db/get-by-id conn :profile profile-id)]
(->> emails (->> emails
(map (fn [email] (map (fn [email]
(-> params (-> params
@ -1024,21 +1031,9 @@
(assoc :profile profile) (assoc :profile profile)
(assoc :email email) (assoc :email email)
(assoc :role role)))) (assoc :role role))))
(run! (partial create-invitation cfg))) (run! (partial create-invitation cfg))))
(run! (partial quotes/check-quote! conn) (vary-meta team assoc ::audit/props {:invitations (count emails)})))
(list {::quotes/id ::quotes/teams-per-profile
::quotes/profile-id profile-id}
{::quotes/id ::quotes/invitations-per-team
::quotes/profile-id profile-id
::quotes/team-id (:id team)
::quotes/incr (count emails)}
{::quotes/id ::quotes/profiles-per-team
::quotes/profile-id profile-id
::quotes/team-id (:id team)
::quotes/incr (count emails)}))
(vary-meta team assoc ::audit/props {:invitations (count emails)})))))
;; --- Query: get-team-invitation-token ;; --- Query: get-team-invitation-token

View file

@ -37,14 +37,12 @@
::doc/added "1.15" ::doc/added "1.15"
::doc/module :auth ::doc/module :auth
::sm/params schema:verify-token} ::sm/params schema:verify-token}
[{:keys [::db/pool] :as cfg} {:keys [token] :as params}] [cfg {:keys [token] :as params}]
(db/with-atomic [conn pool] (let [claims (tokens/verify (::setup/props cfg) {:token token})]
(let [claims (tokens/verify (::setup/props cfg) {:token token}) (db/tx-run! cfg process-token params claims)))
cfg (assoc cfg :conn conn)]
(process-token cfg params claims))))
(defmethod process-token :change-email (defmethod process-token :change-email
[{:keys [conn] :as cfg} _params {:keys [profile-id email] :as claims}] [{:keys [::db/conn] :as cfg} _params {:keys [profile-id email] :as claims}]
(let [email (profile/clean-email email)] (let [email (profile/clean-email email)]
(when (profile/get-profile-by-email conn email) (when (profile/get-profile-by-email conn email)
(ex/raise :type :validation (ex/raise :type :validation
@ -60,7 +58,7 @@
::audit/profile-id profile-id}))) ::audit/profile-id profile-id})))
(defmethod process-token :verify-email (defmethod process-token :verify-email
[{:keys [conn] :as cfg} _ {:keys [profile-id] :as claims}] [{:keys [::db/conn] :as cfg} _ {:keys [profile-id] :as claims}]
(let [profile (profile/get-profile conn profile-id) (let [profile (profile/get-profile conn profile-id)
claims (assoc claims :profile profile)] claims (assoc claims :profile profile)]
@ -81,14 +79,14 @@
::audit/profile-id (:id profile)})))) ::audit/profile-id (:id profile)}))))
(defmethod process-token :auth (defmethod process-token :auth
[{:keys [conn] :as cfg} _params {:keys [profile-id] :as claims}] [{:keys [::db/conn] :as cfg} _params {:keys [profile-id] :as claims}]
(let [profile (profile/get-profile conn profile-id)] (let [profile (profile/get-profile conn profile-id)]
(assoc claims :profile profile))) (assoc claims :profile profile)))
;; --- Team Invitation ;; --- Team Invitation
(defn- accept-invitation (defn- accept-invitation
[{:keys [conn] :as cfg} {:keys [team-id role member-email] :as claims} invitation member] [{:keys [::db/conn] :as cfg} {:keys [team-id role member-email] :as claims} invitation member]
(let [;; Update the role if there is an invitation (let [;; Update the role if there is an invitation
role (or (some-> invitation :role keyword) role) role (or (some-> invitation :role keyword) role)
params (merge params (merge
@ -101,8 +99,7 @@
(ex/raise :type :restriction (ex/raise :type :restriction
:code :profile-blocked)) :code :profile-blocked))
(quotes/check-quote! conn (quotes/check! cfg {::quotes/id ::quotes/profiles-per-team
{::quotes/id ::quotes/profiles-per-team
::quotes/profile-id (:id member) ::quotes/profile-id (:id member)
::quotes/team-id team-id}) ::quotes/team-id team-id})
@ -140,7 +137,7 @@
(sm/lazy-validator schema:team-invitation-claims)) (sm/lazy-validator schema:team-invitation-claims))
(defmethod process-token :team-invitation (defmethod process-token :team-invitation
[{:keys [conn] :as cfg} [{:keys [::db/conn] :as cfg}
{:keys [::rpc/profile-id token] :as params} {:keys [::rpc/profile-id token] :as params}
{:keys [member-id team-id member-email] :as claims}] {:keys [member-id team-id member-email] :as claims}]

View file

@ -7,16 +7,13 @@
(ns app.rpc.quotes (ns app.rpc.quotes
"Penpot resource usage quotes." "Penpot resource usage quotes."
(: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.schema :as sm] [app.common.schema :as sm]
[app.common.spec :as us]
[app.config :as cf] [app.config :as cf]
[app.db :as db] [app.db :as db]
[app.util.time :as dt] [app.util.time :as dt]
[app.worker :as wrk] [app.worker :as wrk]
[clojure.spec.alpha :as s]
[cuerdas.core :as str])) [cuerdas.core :as str]))
(defmulti check-quote ::id) (defmulti check-quote ::id)
@ -34,6 +31,9 @@
[::id :keyword] [::id :keyword]
[::profile-id ::sm/uuid]]) [::profile-id ::sm/uuid]])
(def valid-quote?
(sm/lazy-validator schema:quote))
(def ^:private enabled (volatile! true)) (def ^:private enabled (volatile! true))
(defn enable! (defn enable!
@ -46,20 +46,31 @@
[] []
(vswap! enabled (constantly false))) (vswap! enabled (constantly false)))
(defn check-quote! (defn- check
[ds quote] [cfg quote]
(dm/assert! (let [quote (merge cfg quote)
"expected valid quote map" id (::id quote)]
(sm/validate schema:quote quote))
(when-not (valid-quote? quote)
(ex/raise :type :internal
:code :invalid-quote-definition
:hint "found invalid data for quote schema"
:quote (name id)))
(-> (assoc quote ::target (name id))
(check-quote))))
(defn check!
([cfg]
(when (contains? cf/flags :quotes) (when (contains? cf/flags :quotes)
(when @enabled (when @enabled
;; This approach add flexibility on how and where the (db/run! cfg check {}))))
;; check-quote! can be called (in or out of transaction)
(db/run! ds (fn [cfg] ([cfg & others]
(-> (merge cfg quote) (when (contains? cf/flags :quotes)
(assoc ::target (name (::id quote))) (when @enabled
(check-quote))))))) (db/run! cfg (fn [cfg]
(run! (partial check cfg) others)))))))
(defn- send-notification! (defn- send-notification!
[{:keys [::db/conn] :as params}] [{:keys [::db/conn] :as params}]
@ -100,7 +111,7 @@
(map :quote) (map :quote)
(reduce max (- Integer/MAX_VALUE))) (reduce max (- Integer/MAX_VALUE)))
quote (if (pos? quote) quote default) quote (if (pos? quote) quote default)
total (->> (db/exec! conn count-sql) first :total)] total (:total (db/exec-one! conn count-sql))]
(when (> (+ total incr) quote) (when (> (+ total incr) quote)
(if (contains? cf/flags :soft-quotes) (if (contains? cf/flags :soft-quotes)
@ -112,72 +123,81 @@
:count total))))) :count total)))))
(def ^:private sql:get-quotes-1 (def ^:private sql:get-quotes-1
"select id, quote from usage_quote "SELECT id, quote
where target = ? FROM usage_quote
and profile_id = ? WHERE target = ?
and team_id is null AND profile_id = ?
and project_id is null AND team_id IS NULL
and file_id is null;") AND project_id IS NULL
AND file_id IS NULL;")
(def ^:private sql:get-quotes-2 (def ^:private sql:get-quotes-2
"select id, quote from usage_quote "SELECT id, quote
where target = ? FROM usage_quote
and ((team_id = ? and (profile_id = ? or profile_id is null)) or WHERE target = ?
(profile_id = ? and team_id is null and project_id is null and file_id is null));") AND ((team_id = ? AND (profile_id = ? OR profile_id IS NULL)) OR
(profile_id = ? AND team_id IS NULL AND project_id IS NULL AND file_id IS NULL));")
(def ^:private sql:get-quotes-3 (def ^:private sql:get-quotes-3
"select id, quote from usage_quote "SELECT id, quote
where target = ? FROM usage_quote
and ((project_id = ? and (profile_id = ? or profile_id is null)) or WHERE target = ?
(team_id = ? and (profile_id = ? or profile_id is null)) or AND ((project_id = ? AND (profile_id = ? OR profile_id IS NULL)) OR
(profile_id = ? and team_id is null and project_id is null and file_id is null));") (team_id = ? AND (profile_id = ? OR profile_id IS NULL)) OR
(profile_id = ? AND team_id IS NULL AND project_id IS NULL AND file_id IS NULL));")
(def ^:private sql:get-quotes-4 (def ^:private sql:get-quotes-4
"select id, quote from usage_quote "SELECT id, quote
where target = ? FROM usage_quote
and ((file_id = ? and (profile_id = ? or profile_id is null)) or WHERE target = ?
(project_id = ? and (profile_id = ? or profile_id is null)) or AND ((file_id = ? AND (profile_id = ? OR profile_id IS NULL)) OR
(team_id = ? and (profile_id = ? or profile_id is null)) or (project_id = ? AND (profile_id = ? OR profile_id IS NULL)) OR
(profile_id = ? and team_id is null and project_id is null and file_id is null));") (team_id = ? AND (profile_id = ? OR profile_id IS NULL)) OR
(profile_id = ? AND team_id IS NULL AND project_id IS NULL AND file_id IS NULL));")
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; QUOTE: TEAMS-PER-PROFILE ;; QUOTE: TEAMS-PER-PROFILE
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def ^:private sql:get-teams-per-profile (def ^:private schema:teams-per-profile
"select count(*) as total [:map [::profile-id ::sm/uuid]])
from team_profile_rel
where profile_id = ?")
(s/def ::profile-id ::us/uuid) (def ^:private valid-teams-per-profile-quote?
(s/def ::teams-per-profile (sm/lazy-validator schema:teams-per-profile))
(s/keys :req [::profile-id ::target]))
(def ^:private sql:get-teams-per-profile
"SELECT count(*) AS total
FROM team_profile_rel
WHERE profile_id = ?")
(defmethod check-quote ::teams-per-profile (defmethod check-quote ::teams-per-profile
[{:keys [::profile-id ::target] :as quote}] [{:keys [::profile-id ::target] :as quote}]
(us/assert! ::teams-per-profile quote) (assert (valid-teams-per-profile-quote? quote) "invalid quote parameters")
(-> quote (-> quote
(assoc ::default (cf/get :quotes-teams-per-profile Integer/MAX_VALUE)) (assoc ::default (cf/get :quotes-teams-per-profile Integer/MAX_VALUE))
(assoc ::quote-sql [sql:get-quotes-1 target profile-id]) (assoc ::quote-sql [sql:get-quotes-1 target profile-id])
(assoc ::count-sql [sql:get-teams-per-profile profile-id]) (assoc ::count-sql [sql:get-teams-per-profile profile-id])
(generic-check!))) (generic-check!)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; QUOTE: ACCESS-TOKENS-PER-PROFILE ;; QUOTE: ACCESS-TOKENS-PER-PROFILE
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def ^:private sql:get-access-tokens-per-profile (def ^:private schema:access-tokens-per-profile
"select count(*) as total [:map [::profile-id ::sm/uuid]])
from access_token
where profile_id = ?")
(s/def ::access-tokens-per-profile (def ^:private valid-access-tokens-per-profile-quote?
(s/keys :req [::profile-id ::target])) (sm/lazy-validator schema:access-tokens-per-profile))
(def ^:private sql:get-access-tokens-per-profile
"SELECT count(*) AS total
FROM access_token
WHERE profile_id = ?")
(defmethod check-quote ::access-tokens-per-profile (defmethod check-quote ::access-tokens-per-profile
[{:keys [::profile-id ::target] :as quote}] [{:keys [::profile-id ::target] :as quote}]
(us/assert! ::access-tokens-per-profile quote) (assert (valid-access-tokens-per-profile-quote? quote) "invalid quote parameters")
(-> quote (-> quote
(assoc ::default (cf/get :quotes-access-tokens-per-profile Integer/MAX_VALUE)) (assoc ::default (cf/get :quotes-access-tokens-per-profile Integer/MAX_VALUE))
(assoc ::quote-sql [sql:get-quotes-1 target profile-id]) (assoc ::quote-sql [sql:get-quotes-1 target profile-id])
@ -188,40 +208,51 @@
;; QUOTE: PROJECTS-PER-TEAM ;; QUOTE: PROJECTS-PER-TEAM
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def ^:private sql:get-projects-per-team (def ^:private schema:projects-per-team
"select count(*) as total [:map
from project as p [::profile-id ::sm/uuid]
where p.team_id = ? [::team-id ::sm/uuid]])
and p.deleted_at is null")
(s/def ::team-id ::us/uuid) (def ^:private valid-projects-per-team-quote?
(s/def ::projects-per-team (sm/lazy-validator schema:projects-per-team))
(s/keys :req [::profile-id ::team-id ::target]))
(def ^:private sql:get-projects-per-team
"SELECT count(*) AS total
FROM project AS p
WHERE p.team_id = ?
AND p.deleted_at IS NULL")
(defmethod check-quote ::projects-per-team (defmethod check-quote ::projects-per-team
[{:keys [::profile-id ::team-id ::target] :as quote}] [{:keys [::profile-id ::team-id ::target] :as quote}]
(assert (valid-projects-per-team-quote? quote) "invalid quote parameters")
(-> quote (-> quote
(assoc ::default (cf/get :quotes-projects-per-team Integer/MAX_VALUE)) (assoc ::default (cf/get :quotes-projects-per-team Integer/MAX_VALUE))
(assoc ::quote-sql [sql:get-quotes-2 target team-id profile-id profile-id]) (assoc ::quote-sql [sql:get-quotes-2 target team-id profile-id profile-id])
(assoc ::count-sql [sql:get-projects-per-team team-id]) (assoc ::count-sql [sql:get-projects-per-team team-id])
(generic-check!))) (generic-check!)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; QUOTE: FONT-VARIANTS-PER-TEAM ;; QUOTE: FONT-VARIANTS-PER-TEAM
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def ^:private sql:get-font-variants-per-team (def ^:private schema:font-variants-per-team
"select count(*) as total [:map
from team_font_variant as v [::profile-id ::sm/uuid]
where v.team_id = ?") [::team-id ::sm/uuid]])
(s/def ::font-variants-per-team (def ^:private valid-font-variant-per-team-quote?
(s/keys :req [::profile-id ::team-id ::target])) (sm/lazy-validator schema:font-variants-per-team))
(def ^:private sql:get-font-variants-per-team
"SELECT count(*) AS total
FROM team_font_variant AS v
WHERE v.team_id = ?")
(defmethod check-quote ::font-variants-per-team (defmethod check-quote ::font-variants-per-team
[{:keys [::profile-id ::team-id ::target] :as quote}] [{:keys [::profile-id ::team-id ::target] :as quote}]
(us/assert! ::font-variants-per-team quote) (assert (valid-font-variant-per-team-quote? quote) "invalid quote parameters")
(-> quote (-> quote
(assoc ::default (cf/get :quotes-font-variants-per-team Integer/MAX_VALUE)) (assoc ::default (cf/get :quotes-font-variants-per-team Integer/MAX_VALUE))
(assoc ::quote-sql [sql:get-quotes-2 target team-id profile-id profile-id]) (assoc ::quote-sql [sql:get-quotes-2 target team-id profile-id profile-id])
@ -233,70 +264,86 @@
;; QUOTE: INVITATIONS-PER-TEAM ;; QUOTE: INVITATIONS-PER-TEAM
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def ^:private sql:get-invitations-per-team (def ^:private schema:invitations-per-team
"select count(*) as total [:map
from team_invitation [::profile-id ::sm/uuid]
where team_id = ?") [::team-id ::sm/uuid]])
(s/def ::invitations-per-team (def ^:private valid-invitations-per-team-quote?
(s/keys :req [::profile-id ::team-id ::target])) (sm/lazy-validator schema:invitations-per-team))
(def ^:private sql:get-invitations-per-team
"SELECT count(*) AS total
FROM team_invitation
WHERE team_id = ?")
(defmethod check-quote ::invitations-per-team (defmethod check-quote ::invitations-per-team
[{:keys [::profile-id ::team-id ::target] :as quote}] [{:keys [::profile-id ::team-id ::target] :as quote}]
(us/assert! ::invitations-per-team quote) (assert (valid-invitations-per-team-quote? quote) "invalid quote parameters")
(-> quote (-> quote
(assoc ::default (cf/get :quotes-invitations-per-team Integer/MAX_VALUE)) (assoc ::default (cf/get :quotes-invitations-per-team Integer/MAX_VALUE))
(assoc ::quote-sql [sql:get-quotes-2 target team-id profile-id profile-id]) (assoc ::quote-sql [sql:get-quotes-2 target team-id profile-id profile-id])
(assoc ::count-sql [sql:get-invitations-per-team team-id]) (assoc ::count-sql [sql:get-invitations-per-team team-id])
(generic-check!))) (generic-check!)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; QUOTE: PROFILES-PER-TEAM ;; QUOTE: PROFILES-PER-TEAM
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def ^:private schema:profiles-per-team
[:map
[::profile-id ::sm/uuid]
[::team-id ::sm/uuid]])
(def ^:private valid-profiles-per-team-quote?
(sm/lazy-validator schema:profiles-per-team))
(def ^:private sql:get-profiles-per-team (def ^:private sql:get-profiles-per-team
"select (select count(*) "SELECT (SELECT count(*)
from team_profile_rel FROM team_profile_rel
where team_id = ?) + WHERE team_id = ?) +
(select count(*) (SELECT count(*)
from team_invitation FROM team_invitation
where team_id = ? WHERE team_id = ?
and valid_until > now()) as total;") AND valid_until > now()) AS total;")
;; NOTE: the total number of profiles is determined by the number of ;; NOTE: the total number of profiles is determined by the number of
;; effective members plus ongoing valid invitations. ;; effective members plus ongoing valid invitations.
(s/def ::profiles-per-team
(s/keys :req [::profile-id ::team-id ::target]))
(defmethod check-quote ::profiles-per-team (defmethod check-quote ::profiles-per-team
[{:keys [::profile-id ::team-id ::target] :as quote}] [{:keys [::profile-id ::team-id ::target] :as quote}]
(us/assert! ::profiles-per-team quote) (assert (valid-profiles-per-team-quote? quote) "invalid quote parameters")
(-> quote (-> quote
(assoc ::default (cf/get :quotes-profiles-per-team Integer/MAX_VALUE)) (assoc ::default (cf/get :quotes-profiles-per-team Integer/MAX_VALUE))
(assoc ::quote-sql [sql:get-quotes-2 target team-id profile-id profile-id]) (assoc ::quote-sql [sql:get-quotes-2 target team-id profile-id profile-id])
(assoc ::count-sql [sql:get-profiles-per-team team-id team-id]) (assoc ::count-sql [sql:get-profiles-per-team team-id team-id])
(generic-check!))) (generic-check!)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; QUOTE: FILES-PER-PROJECT ;; QUOTE: FILES-PER-PROJECT
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def ^:private sql:get-files-per-project (def ^:private schema:files-per-project
"select count(*) as total [:map
from file as f [::profile-id ::sm/uuid]
where f.project_id = ? [::project-id ::sm/uuid]
and f.deleted_at is null") [::team-id ::sm/uuid]])
(s/def ::project-id ::us/uuid) (def ^:private valid-files-per-project-quote?
(s/def ::files-per-project (sm/lazy-validator schema:files-per-project))
(s/keys :req [::profile-id ::project-id ::team-id ::target]))
(def ^:private sql:get-files-per-project
"SELECT count(*) AS total
FROM file AS f
WHERE f.project_id = ?
AND f.deleted_at IS NULL")
(defmethod check-quote ::files-per-project (defmethod check-quote ::files-per-project
[{:keys [::profile-id ::project-id ::team-id ::target] :as quote}] [{:keys [::profile-id ::project-id ::team-id ::target] :as quote}]
(us/assert! ::files-per-project quote) (assert (valid-files-per-project-quote? quote) "invalid quote parameters")
(-> quote (-> quote
(assoc ::default (cf/get :quotes-files-per-project Integer/MAX_VALUE)) (assoc ::default (cf/get :quotes-files-per-project Integer/MAX_VALUE))
(assoc ::quote-sql [sql:get-quotes-3 target project-id profile-id team-id profile-id profile-id]) (assoc ::quote-sql [sql:get-quotes-3 target project-id profile-id team-id profile-id profile-id])
@ -307,17 +354,24 @@
;; QUOTE: COMMENT-THREADS-PER-FILE ;; QUOTE: COMMENT-THREADS-PER-FILE
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def ^:private sql:get-comment-threads-per-file (def ^:private schema:comment-threads-per-file
"select count(*) as total [:map
from comment_thread as ct [::profile-id ::sm/uuid]
where ct.file_id = ?") [::project-id ::sm/uuid]
[::team-id ::sm/uuid]])
(s/def ::comment-threads-per-file (def ^:private valid-comment-threads-per-file-quote?
(s/keys :req [::profile-id ::project-id ::team-id ::target])) (sm/lazy-validator schema:comment-threads-per-file))
(def ^:private sql:get-comment-threads-per-file
"SELECT count(*) AS total
FROM comment_thread AS ct
WHERE ct.file_id = ?")
(defmethod check-quote ::comment-threads-per-file (defmethod check-quote ::comment-threads-per-file
[{:keys [::profile-id ::file-id ::team-id ::project-id ::target] :as quote}] [{:keys [::profile-id ::file-id ::team-id ::project-id ::target] :as quote}]
(us/assert! ::files-per-project quote) (assert (valid-comment-threads-per-file-quote? quote) "invalid quote parameters")
(-> quote (-> quote
(assoc ::default (cf/get :quotes-comment-threads-per-file Integer/MAX_VALUE)) (assoc ::default (cf/get :quotes-comment-threads-per-file Integer/MAX_VALUE))
(assoc ::quote-sql [sql:get-quotes-4 target file-id profile-id project-id (assoc ::quote-sql [sql:get-quotes-4 target file-id profile-id project-id
@ -325,23 +379,28 @@
(assoc ::count-sql [sql:get-comment-threads-per-file file-id]) (assoc ::count-sql [sql:get-comment-threads-per-file file-id])
(generic-check!))) (generic-check!)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; QUOTE: COMMENTS-PER-FILE ;; QUOTE: COMMENTS-PER-FILE
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def ^:private sql:get-comments-per-file (def ^:private schema:comments-per-file
"select count(*) as total [:map
from comment as c [::profile-id ::sm/uuid]
join comment_thread as ct on (ct.id = c.thread_id) [::project-id ::sm/uuid]
where ct.file_id = ?") [::team-id ::sm/uuid]])
(s/def ::comments-per-file (def ^:private valid-comments-per-file-quote?
(s/keys :req [::profile-id ::project-id ::team-id ::target])) (sm/lazy-validator schema:comments-per-file))
(def ^:private sql:get-comments-per-file
"SELECT count(*) AS total
FROM comment AS c
JOIN comment_thread AS ct ON (ct.id = c.thread_id)
WHERE ct.file_id = ?")
(defmethod check-quote ::comments-per-file (defmethod check-quote ::comments-per-file
[{:keys [::profile-id ::file-id ::team-id ::project-id ::target] :as quote}] [{:keys [::profile-id ::file-id ::team-id ::project-id ::target] :as quote}]
(us/assert! ::files-per-project quote) (assert (valid-comments-per-file-quote? quote) "invalid quote parameters")
(-> quote (-> quote
(assoc ::default (cf/get :quotes-comments-per-file Integer/MAX_VALUE)) (assoc ::default (cf/get :quotes-comments-per-file Integer/MAX_VALUE))
(assoc ::quote-sql [sql:get-quotes-4 target file-id profile-id project-id (assoc ::quote-sql [sql:get-quotes-4 target file-id profile-id project-id

View file

@ -21,7 +21,7 @@
(t/use-fixtures :each th/database-reset) (t/use-fixtures :each th/database-reset)
(t/deftest ttf-font-upload-1 (t/deftest ttf-font-upload-1
(with-mocks [mock {:target 'app.rpc.quotes/check-quote! :return nil}] (with-mocks [mock {:target 'app.rpc.quotes/check! :return nil}]
(let [prof (th/create-profile* 1 {:is-active true}) (let [prof (th/create-profile* 1 {:is-active true})
team-id (:default-team-id prof) team-id (:default-team-id prof)
proj-id (:default-project-id prof) proj-id (:default-project-id prof)

View file

@ -176,6 +176,10 @@
(= :max-invitations-by-request code)) (= :max-invitations-by-request code))
(swap! error-text (tr "errors.maximum-invitations-by-request-reached" (:threshold error))) (swap! error-text (tr "errors.maximum-invitations-by-request-reached" (:threshold error)))
(and (= :restriction type)
(= :max-quote-reached code))
(swap! error-text (tr "errors.max-quote-reached" (:target error)))
(or (= :member-is-muted code) (or (= :member-is-muted code)
(= :email-has-permanent-bounces code) (= :email-has-permanent-bounces code)
(= :email-has-complaints code)) (= :email-has-complaints code))

View file

@ -98,6 +98,10 @@
(= :max-invitations-by-request code)) (= :max-invitations-by-request code))
(swap! error* (tr "errors.maximum-invitations-by-request-reached" (:threshold error))) (swap! error* (tr "errors.maximum-invitations-by-request-reached" (:threshold error)))
(and (= :restriction type)
(= :max-quote-reached code))
(swap! error* (tr "errors.max-quote-reached" (:target error)))
(or (= :member-is-muted code) (or (= :member-is-muted code)
(= :email-has-permanent-bounces code) (= :email-has-permanent-bounces code)
(= :email-has-complaints code)) (= :email-has-complaints code))