♻️ 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

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

View file

@ -297,38 +297,38 @@
[:frame-id ::sm/uuid]
[:share-id {:optional true} [:maybe ::sm/uuid]]])
;; FIXME: relax transaction requirements
(sv/defmethod ::create-comment-thread
{::doc/added "1.15"
::webhooks/event? true
::rtry/enabled true
::rtry/when rtry/conflict-exception?
::sm/params schema:create-comment-thread}
[cfg {:keys [::rpc/profile-id ::rpc/request-at file-id page-id share-id position content frame-id]}]
::sm/params schema:create-comment-thread
::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)
(let [{:keys [team-id project-id page-name]} (get-file conn file-id page-id)]
(files/check-comment-permissions! cfg profile-id file-id share-id)
(run! (partial quotes/check-quote! cfg)
(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}))
(let [{:keys [team-id project-id page-name]} (get-file conn file-id page-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})))))
(-> cfg
(assoc ::quotes/profile-id profile-id)
(assoc ::quotes/team-id team-id)
(assoc ::quotes/project-id project-id)
(assoc ::quotes/file-id file-id)
(quotes/check! {::quotes/id ::quotes/comment-threads-per-file}
{::quotes/id ::quotes/comments-per-file}))
(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
[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)]
(files/check-comment-permissions! conn profile-id file-id share-id)
(quotes/check-quote! conn
{::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})
(quotes/check! cfg {::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})
;; Update the page-name cached attribute on comment thread table.
(when (not= page-name (:page-name thread))

View file

@ -98,46 +98,49 @@
{::doc/added "1.17"
::doc/module :files
::webhooks/event? true
::sm/params schema:create-file}
[cfg {:keys [::rpc/profile-id project-id] :as params}]
(db/tx-run! cfg
(fn [{:keys [::db/conn] :as cfg}]
(projects/check-edition-permissions! conn profile-id project-id)
(let [team (teams/get-team conn
:profile-id profile-id
:project-id project-id)
team-id (:id team)
::sm/params schema:create-file
::db/transaction true}
[{:keys [::db/conn] :as cfg} {:keys [::rpc/profile-id project-id] :as params}]
(projects/check-edition-permissions! conn profile-id project-id)
(let [team (teams/get-team conn
:profile-id profile-id
:project-id project-id)
team-id (:id team)
;; When we create files, we only need to respect the team
;; features, because some features can be enabled
;; globally, but the team is still not migrated properly.
features (-> (cfeat/get-team-enabled-features cf/flags team)
(cfeat/check-client-features! (:features params)))
;; When we create files, we only need to respect the team
;; features, because some features can be enabled
;; globally, but the team is still not migrated properly.
features (-> (cfeat/get-team-enabled-features cf/flags team)
(cfeat/check-client-features! (:features params)))
;; We also include all no migration features declared by
;; client; that enables the ability to enable a runtime
;; feature on frontend and make it permanent on file
features (-> (:features params #{})
(set/intersection cfeat/no-migration-features)
(set/union features))
;; We also include all no migration features declared by
;; client; that enables the ability to enable a runtime
;; feature on frontend and make it permanent on file
features (-> (:features params #{})
(set/intersection cfeat/no-migration-features)
(set/union features))
params (-> params
(assoc :profile-id profile-id)
(assoc :features features))]
params (-> params
(assoc :profile-id profile-id)
(assoc :features features))]
(run! (partial quotes/check-quote! conn)
(list {::quotes/id ::quotes/files-per-project
::quotes/team-id team-id
::quotes/profile-id profile-id
::quotes/project-id project-id}))
(quotes/check! cfg {::quotes/id ::quotes/files-per-project
::quotes/team-id team-id
::quotes/profile-id profile-id
::quotes/project-id project-id})
;; When newly computed features does not match exactly with
;; 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})))
;; 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
(-> (create-file cfg params)
(vary-meta assoc ::audit/props {:team-id team-id}))))))
;; When newly computed features does not match exactly with
;; 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-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
{::doc/added "1.18"
::climit/id [[:process-font/by-profile ::rpc/profile-id]
@ -96,9 +99,9 @@
(db/tx-run! cfg
(fn [{:keys [::db/conn] :as cfg}]
(teams/check-edition-permissions! conn profile-id team-id)
(quotes/check-quote! conn {::quotes/id ::quotes/font-variants-per-team
::quotes/profile-id profile-id
::quotes/team-id team-id})
(quotes/check! cfg {::quotes/id ::quotes/font-variants-per-team
::quotes/profile-id profile-id
::quotes/team-id team-id})
(create-font-variant cfg (assoc params :profile-id profile-id)))))
(defn create-font-variant

View file

@ -168,6 +168,17 @@
;; --- 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
[:map {:title "create-project"}
[:team-id ::sm/uuid]
@ -178,23 +189,15 @@
{::doc/added "1.18"
::webhooks/event? true
::sm/params schema:create-project}
[{:keys [::db/pool] :as 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})
[cfg {:keys [::rpc/profile-id team-id] :as params}]
(let [params (assoc params :profile-id profile-id)
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))))
(teams/check-edition-permissions! cfg profile-id team-id)
(quotes/check! cfg {::quotes/id ::quotes/projects-per-team
::quotes/profile-id profile-id
::quotes/team-id team-id})
(let [params (assoc params :profile-id profile-id)]
(db/tx-run! cfg create-project params)))
;; --- MUTATION: Toggle Project Pin

View file

@ -401,19 +401,22 @@
(sv/defmethod ::create-team
{::doc/added "1.17"
::sm/params schema:create-team}
::sm/params schema:create-team
::db/transaction true}
[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)
(cfeat/check-client-features! (:features params)))
team (create-team cfg (assoc params
:profile-id profile-id
:features features))]
(with-meta team
{::audit/props {:id (:id team)}})))))
(quotes/check! cfg {::quotes/id ::quotes/teams-per-profile
::quotes/profile-id profile-id})
(let [features (-> (cfeat/get-enabled-features cf/flags)
(cfeat/check-client-features! (:features params)))
params (-> params
(assoc :profile-id profile-id)
(assoc :features features))
team (create-team cfg params)]
(with-meta team
{::audit/props {:id (:id team)}})))
(defn create-team
"This is a complete team creation process, it creates the team
@ -867,10 +870,11 @@
(ex/raise :type :restriction
:code :profile-blocked))
(quotes/check-quote! conn
{::quotes/id ::quotes/profiles-per-team
::quotes/profile-id (:id member)
::quotes/team-id team-id})
(quotes/check!
{::db/conn conn
::quotes/id ::quotes/profiles-per-team
::quotes/profile-id (:id member)
::quotes/team-id team-id})
;; Insert the member to the team
(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
join the team."
{::doc/added "1.17"
::sm/params schema:create-team-invitations}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id emails role] :as params}]
(db/with-atomic [conn pool]
(let [perms (get-permissions conn profile-id team-id)
profile (db/get-by-id conn :profile profile-id)
team (db/get-by-id conn :team team-id)
emails (into #{} (map profile/clean-email) emails)]
::sm/params schema:create-team-invitations
::db/transaction true}
[{:keys [::db/conn] :as cfg} {:keys [::rpc/profile-id team-id emails role] :as params}]
(let [perms (get-permissions conn profile-id team-id)
profile (db/get-by-id conn :profile profile-id)
team (db/get-by-id conn :team team-id)
emails (into #{} (map profile/clean-email) emails)]
(when (> (count emails) max-invitations-by-request-threshold)
(ex/raise :type :validation
:code :max-invitations-by-request
:hint "the maximum of invitation on single request is reached"
:threshold max-invitations-by-request-threshold))
(when-not (:is-admin perms)
(ex/raise :type :validation
:code :insufficient-permissions))
(run! (partial quotes/check-quote! conn)
(list {::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)}))
(when (> (count emails) max-invitations-by-request-threshold)
(ex/raise :type :validation
:code :max-invitations-by-request
:hint "the maximum of invitation on single request is reached"
:threshold max-invitations-by-request-threshold))
(when-not (:is-admin perms)
(ex/raise :type :validation
:code :insufficient-permissions))
(-> cfg
(assoc ::quotes/profile-id profile-id)
(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-valid-email-muted conn profile)
;; Check if the current profile is allowed to send emails
(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))
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)))
invitations (into #{}
(comp
;; We don't re-send inviation to already existing members
(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))
invitations (into #{}
(comp
;; We don't re-send inviation to already existing members
(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)}})))))
(with-meta {:total (count invitations)
:invitations invitations}
{::audit/props {:invitations (count invitations)}}))))
;; --- Mutation: Create Team & Invite Members
@ -987,58 +989,51 @@
(sv/defmethod ::create-team-with-invitations
{::doc/added "1.17"
::sm/params schema:create-team-with-invitations}
[cfg {:keys [::rpc/profile-id emails role name] :as params}]
::sm/params schema:create-team-with-invitations
::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
(fn [{:keys [::db/conn] :as cfg}]
(let [features (-> (cfeat/get-enabled-features cf/flags)
(cfeat/check-client-features! (:features params)))
params (-> params
(assoc :profile-id profile-id)
(assoc :features features))
params (-> params
(assoc :profile-id profile-id)
(assoc :features features))
team (create-team cfg params)
emails (into #{} (map profile/clean-email) emails)]
cfg (assoc cfg ::db/conn conn)
team (create-team cfg params)
profile (db/get-by-id conn :profile profile-id)
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)
(ex/raise :type :validation
:code :max-invitations-by-request
:hint "the maximum of invitation on single request is reached"
:threshold max-invitations-by-request-threshold))
(when (> (count emails) max-invitations-by-request-threshold)
(ex/raise :type :validation
:code :max-invitations-by-request
:hint "the maximum of invitation on single request is reached"
:threshold max-invitations-by-request-threshold))
(let [props {:name name :features features}
event (-> (audit/event-from-rpc-params params)
(assoc ::audit/name "create-team")
(assoc ::audit/props props))]
(audit/submit! cfg event))
(let [props {:name name :features features}
event (-> (audit/event-from-rpc-params params)
(assoc ::audit/name "create-team")
(assoc ::audit/props props))]
(audit/submit! cfg event))
;; Create invitations for all provided emails.
(->> emails
(map (fn [email]
(-> params
(assoc :team team)
(assoc :profile profile)
(assoc :email email)
(assoc :role role))))
(run! (partial create-invitation cfg)))
;; Create invitations for all provided emails.
(let [profile (db/get-by-id conn :profile profile-id)]
(->> emails
(map (fn [email]
(-> params
(assoc :team team)
(assoc :profile profile)
(assoc :email email)
(assoc :role role))))
(run! (partial create-invitation cfg))))
(run! (partial quotes/check-quote! conn)
(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)})))))
(vary-meta team assoc ::audit/props {:invitations (count emails)})))
;; --- Query: get-team-invitation-token

View file

@ -37,14 +37,12 @@
::doc/added "1.15"
::doc/module :auth
::sm/params schema:verify-token}
[{:keys [::db/pool] :as cfg} {:keys [token] :as params}]
(db/with-atomic [conn pool]
(let [claims (tokens/verify (::setup/props cfg) {:token token})
cfg (assoc cfg :conn conn)]
(process-token cfg params claims))))
[cfg {:keys [token] :as params}]
(let [claims (tokens/verify (::setup/props cfg) {:token token})]
(db/tx-run! cfg process-token params claims)))
(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)]
(when (profile/get-profile-by-email conn email)
(ex/raise :type :validation
@ -60,7 +58,7 @@
::audit/profile-id profile-id})))
(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)
claims (assoc claims :profile profile)]
@ -81,14 +79,14 @@
::audit/profile-id (:id profile)}))))
(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)]
(assoc claims :profile profile)))
;; --- Team 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
role (or (some-> invitation :role keyword) role)
params (merge
@ -101,10 +99,9 @@
(ex/raise :type :restriction
:code :profile-blocked))
(quotes/check-quote! conn
{::quotes/id ::quotes/profiles-per-team
::quotes/profile-id (:id member)
::quotes/team-id team-id})
(quotes/check! cfg {::quotes/id ::quotes/profiles-per-team
::quotes/profile-id (:id member)
::quotes/team-id team-id})
;; Insert the invited member to the team
(db/insert! conn :team-profile-rel params {::db/on-conflict-do-nothing? true})
@ -140,7 +137,7 @@
(sm/lazy-validator schema:team-invitation-claims))
(defmethod process-token :team-invitation
[{:keys [conn] :as cfg}
[{:keys [::db/conn] :as cfg}
{:keys [::rpc/profile-id token] :as params}
{:keys [member-id team-id member-email] :as claims}]