♻️ 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,18 +30,17 @@
: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 :profile-id profile-id
:profile-id profile-id :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/profile-id profile-id})
{::quotes/id ::quotes/access-tokens-per-profile
::quotes/profile-id profile-id}) (db/tx-run! cfg create-access-token profile-id name expiration))
(-> (create-access-token cfg profile-id name expiration)
(decode-row)))))
(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,38 +297,38 @@
[: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)]
(run! (partial quotes/check-quote! cfg) (let [{:keys [team-id project-id page-name]} (get-file conn file-id page-id)]
(list {::quotes/id ::quotes/comment-threads-per-file
::quotes/profile-id profile-id
::quotes/team-id team-id
::quotes/project-id project-id
::quotes/file-id file-id}
{::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 (-> cfg
:profile-id profile-id (assoc ::quotes/profile-id profile-id)
:file-id file-id (assoc ::quotes/team-id team-id)
:page-id page-id (assoc ::quotes/project-id project-id)
:page-name page-name (assoc ::quotes/file-id file-id)
:position position (quotes/check! {::quotes/id ::quotes/comment-threads-per-file}
:content content {::quotes/id ::quotes/comments-per-file}))
:frame-id frame-id})))))
(create-comment-thread conn {:created-at request-at
:profile-id profile-id
:file-id file-id
:page-id page-id
:page-name page-name
:position position
:content content
: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,12 +432,11 @@
{: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 ::quotes/file-id file-id})
::quotes/file-id file-id})
;; Update the page-name cached attribute on comment thread table. ;; Update the page-name cached attribute on comment thread table.
(when (not= page-name (:page-name thread)) (when (not= page-name (:page-name thread))

View file

@ -98,46 +98,49 @@
{::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 :project-id project-id)
:project-id project-id) team-id (:id team)
team-id (:id team)
;; When we create files, we only need to respect the team ;; When we create files, we only need to respect the team
;; features, because some features can be enabled ;; features, because some features can be enabled
;; globally, but the team is still not migrated properly. ;; globally, but the team is still not migrated properly.
features (-> (cfeat/get-team-enabled-features cf/flags team) features (-> (cfeat/get-team-enabled-features cf/flags team)
(cfeat/check-client-features! (:features params))) (cfeat/check-client-features! (:features params)))
;; We also include all no migration features declared by ;; We also include all no migration features declared by
;; client; that enables the ability to enable a runtime ;; client; that enables the ability to enable a runtime
;; feature on frontend and make it permanent on file ;; feature on frontend and make it permanent on file
features (-> (:features params #{}) features (-> (:features params #{})
(set/intersection cfeat/no-migration-features) (set/intersection cfeat/no-migration-features)
(set/union features)) (set/union features))
params (-> params params (-> params
(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}))
;; When newly computed features does not match exactly with ;; FIXME: IMPORTANT: this code can have race
;; the features defined on team row, we update it. ;; conditions, because we have no locks for updating
(when (not= features (:features team)) ;; team so, creating two files concurrently can lead
(let [features (db/create-array conn "text" features)] ;; to lost team features updating
(db/update! conn :team
{:features features}
{:id team-id})))
(-> (create-file cfg params) ;; When newly computed features does not match exactly with
(vary-meta assoc ::audit/props {:team-id team-id})))))) ;; the features defined on team row, we update it.
(when (not= features (:features team))
(let [features (db/create-array conn "text" features)]
(db/update! conn :team
{:features features}
{:id team-id})))
(-> (create-file cfg params)
(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,9 +99,9 @@
(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)))))
(defn create-font-variant (defn create-font-variant

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)
(quotes/check-quote! conn {::quotes/id ::quotes/projects-per-team
::quotes/profile-id profile-id
::quotes/team-id team-id})
(let [params (assoc params :profile-id profile-id) (teams/check-edition-permissions! cfg profile-id team-id)
project (teams/create-project conn params)] (quotes/check! cfg {::quotes/id ::quotes/projects-per-team
(teams/create-project-role conn profile-id (:id project) :owner) ::quotes/profile-id profile-id
(db/insert! conn :team-project-profile-rel ::quotes/team-id team-id})
{:project-id (:id project)
:profile-id profile-id
:team-id team-id
:is-pinned false})
(assoc project :is-pinned false))))
(let [params (assoc params :profile-id profile-id)]
(db/tx-run! cfg create-project params)))
;; --- 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/profile-id profile-id})
(let [features (-> (cfeat/get-enabled-features cf/flags) (quotes/check! cfg {::quotes/id ::quotes/teams-per-profile
(cfeat/check-client-features! (:features params))) ::quotes/profile-id profile-id})
team (create-team cfg (assoc params
:profile-id profile-id (let [features (-> (cfeat/get-enabled-features cf/flags)
:features features))] (cfeat/check-client-features! (:features params)))
(with-meta team params (-> params
{::audit/props {:id (:id team)}}))))) (assoc :profile-id profile-id)
(assoc :features features))
team (create-team cfg params)]
(with-meta 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,10 +870,11 @@
(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/profile-id (:id member) ::quotes/id ::quotes/profiles-per-team
::quotes/team-id team-id}) ::quotes/profile-id (:id member)
::quotes/team-id team-id})
;; Insert the member to the team ;; Insert the member to the team
(db/insert! conn :team-profile-rel params {::db/on-conflict-do-nothing? true}) (db/insert! conn :team-profile-rel params {::db/on-conflict-do-nothing? true})
@ -916,64 +920,62 @@
"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 (> (count emails) max-invitations-by-request-threshold) (when-not (:is-admin perms)
(ex/raise :type :validation (ex/raise :type :validation
:code :max-invitations-by-request :code :insufficient-permissions))
:hint "the maximum of invitation on single request is reached"
:threshold max-invitations-by-request-threshold))
(run! (partial quotes/check-quote! conn) (when (> (count emails) max-invitations-by-request-threshold)
(list {::quotes/id ::quotes/invitations-per-team (ex/raise :type :validation
::quotes/profile-id profile-id :code :max-invitations-by-request
::quotes/team-id (:id team) :hint "the maximum of invitation on single request is reached"
::quotes/incr (count emails)} :threshold max-invitations-by-request-threshold))
{::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) (-> cfg
(ex/raise :type :validation (assoc ::quotes/profile-id profile-id)
:code :insufficient-permissions)) (assoc ::quotes/team-id team-id)
(assoc ::quotes/incr (count emails))
(quotes/check! {::quotes/id ::quotes/invitations-per-team}
{::quotes/id ::quotes/profiles-per-team}))
;; Check if the current profile is allowed to send emails. ;; 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))
emails-to-add (filter #(contains? requested %) emails)
emails (remove #(contains? requested %) emails)
cfg (assoc cfg ::db/conn conn)
members (->> (db/exec! conn [sql:team-members team-id])
(into #{} (map :email)))
(let [requested (into #{} (map :email) (get-valid-requests-email conn team-id)) invitations (into #{}
emails-to-add (filter #(contains? requested %) emails) (comp
emails (remove #(contains? requested %) emails) ;; We don't re-send inviation to already existing members
cfg (assoc cfg ::db/conn conn) (remove (partial contains? members))
members (->> (db/exec! conn [sql:team-members team-id]) (map (fn [email]
(into #{} (map :email))) (-> params
(assoc :email email)
(assoc :team team)
(assoc :profile profile)
(assoc :role role))))
(keep (partial create-invitation cfg)))
emails)]
;; For requested invitations, do not send
;; invitation emails, add the user directly to
;; the team
(doseq [email emails-to-add]
(add-user-to-team conn profile team email role))
invitations (into #{} (with-meta {:total (count invitations)
(comp :invitations invitations}
;; We don't re-send inviation to already existing members {::audit/props {:invitations (count invitations)}}))))
(remove (partial contains? members))
(map (fn [email]
(-> params
(assoc :email email)
(assoc :team team)
(assoc :profile profile)
(assoc :role role))))
(keep (partial create-invitation cfg)))
emails)]
;; For requested invitations, do not send invitation emails, add the user directly to the team
(doseq [email emails-to-add]
(add-user-to-team conn profile team email role))
(with-meta {:total (count invitations)
:invitations invitations}
{::audit/props {:invitations (count invitations)}})))))
;; --- Mutation: Create Team & Invite Members ;; --- Mutation: Create Team & Invite Members
@ -987,58 +989,51 @@
(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}]
(let [features (-> (cfeat/get-enabled-features cf/flags)
(cfeat/check-client-features! (:features params)))
(db/tx-run! cfg params (-> params
(fn [{:keys [::db/conn] :as cfg}] (assoc :profile-id profile-id)
(let [features (-> (cfeat/get-enabled-features cf/flags) (assoc :features features))
(cfeat/check-client-features! (:features params)))
params (-> params team (create-team cfg params)
(assoc :profile-id profile-id) emails (into #{} (map profile/clean-email) emails)]
(assoc :features features))
cfg (assoc cfg ::db/conn conn) (-> cfg
team (create-team cfg params) (assoc ::quotes/profile-id profile-id)
profile (db/get-by-id conn :profile profile-id) (assoc ::quotes/team-id (:id team))
emails (into #{} (map profile/clean-email) emails)] (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
: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))
(let [props {:name name :features features} (let [props {:name name :features features}
event (-> (audit/event-from-rpc-params params) event (-> (audit/event-from-rpc-params params)
(assoc ::audit/name "create-team") (assoc ::audit/name "create-team")
(assoc ::audit/props props))] (assoc ::audit/props props))]
(audit/submit! cfg event)) (audit/submit! cfg event))
;; Create invitations for all provided emails. ;; Create invitations for all provided emails.
(->> emails (let [profile (db/get-by-id conn :profile profile-id)]
(map (fn [email] (->> emails
(-> params (map (fn [email]
(assoc :team team) (-> params
(assoc :profile profile) (assoc :team team)
(assoc :email email) (assoc :profile profile)
(assoc :role role)))) (assoc :email email)
(run! (partial create-invitation cfg))) (assoc :role role))))
(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,10 +99,9 @@
(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})
;; Insert the invited member to the team ;; Insert the invited member to the team
(db/insert! conn :team-profile-rel params {::db/on-conflict-do-nothing? true}) (db/insert! conn :team-profile-rel params {::db/on-conflict-do-nothing? true})
@ -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 (contains? cf/flags :quotes) (when-not (valid-quote? quote)
(when @enabled (ex/raise :type :internal
;; This approach add flexibility on how and where the :code :invalid-quote-definition
;; check-quote! can be called (in or out of transaction) :hint "found invalid data for quote schema"
(db/run! ds (fn [cfg] :quote (name id)))
(-> (merge cfg quote)
(assoc ::target (name (::id quote))) (-> (assoc quote ::target (name id))
(check-quote))))))) (check-quote))))
(defn check!
([cfg]
(when (contains? cf/flags :quotes)
(when @enabled
(db/run! cfg check {}))))
([cfg & others]
(when (contains? cf/flags :quotes)
(when @enabled
(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))