mirror of
https://github.com/penpot/penpot.git
synced 2025-07-16 13:35:20 +02:00
The main issue was the long running gc operation that affects storage objects with deduplication. The long running transacion ends locking some storage object rows which collaterally made operations like import-binfile become blocked indefinitelly because of the same rows (because of deduplication). The solution used in this commit is split operations on small chunks so we no longer use long running transactions that holds too many locks. With this approach we will make a window to work concurrently all operarate the distinct operations that requires locks on the same rows.
278 lines
8.7 KiB
Clojure
278 lines
8.7 KiB
Clojure
;; This Source Code Form is subject to the terms of the Mozilla Public
|
|
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
;;
|
|
;; Copyright (c) KALEIDOS INC
|
|
|
|
(ns app.rpc.commands.projects
|
|
(:require
|
|
[app.common.data.macros :as dm]
|
|
[app.common.spec :as us]
|
|
[app.db :as db]
|
|
[app.db.sql :as-alias sql]
|
|
[app.loggers.audit :as-alias audit]
|
|
[app.loggers.webhooks :as webhooks]
|
|
[app.rpc :as-alias rpc]
|
|
[app.rpc.commands.teams :as teams]
|
|
[app.rpc.doc :as-alias doc]
|
|
[app.rpc.helpers :as rph]
|
|
[app.rpc.permissions :as perms]
|
|
[app.rpc.quotes :as quotes]
|
|
[app.util.services :as sv]
|
|
[app.util.time :as dt]
|
|
[app.worker :as wrk]
|
|
[clojure.spec.alpha :as s]))
|
|
|
|
(s/def ::id ::us/uuid)
|
|
(s/def ::name ::us/string)
|
|
|
|
;; --- Check Project Permissions
|
|
|
|
(def ^:private sql:project-permissions
|
|
"select tpr.is_owner,
|
|
tpr.is_admin,
|
|
tpr.can_edit
|
|
from team_profile_rel as tpr
|
|
inner join project as p on (p.team_id = tpr.team_id)
|
|
where p.id = ?
|
|
and tpr.profile_id = ?
|
|
union all
|
|
select ppr.is_owner,
|
|
ppr.is_admin,
|
|
ppr.can_edit
|
|
from project_profile_rel as ppr
|
|
where ppr.project_id = ?
|
|
and ppr.profile_id = ?")
|
|
|
|
(defn- get-permissions
|
|
[conn profile-id project-id]
|
|
(let [rows (db/exec! conn [sql:project-permissions
|
|
project-id profile-id
|
|
project-id profile-id])
|
|
is-owner (boolean (some :is-owner rows))
|
|
is-admin (boolean (some :is-admin rows))
|
|
can-edit (boolean (some :can-edit rows))]
|
|
(when (seq rows)
|
|
{:is-owner is-owner
|
|
:is-admin (or is-owner is-admin)
|
|
:can-edit (or is-owner is-admin can-edit)
|
|
:can-read true})))
|
|
|
|
(def has-edit-permissions?
|
|
(perms/make-edition-predicate-fn get-permissions))
|
|
|
|
(def has-read-permissions?
|
|
(perms/make-read-predicate-fn get-permissions))
|
|
|
|
(def check-edition-permissions!
|
|
(perms/make-check-fn has-edit-permissions?))
|
|
|
|
(def check-read-permissions!
|
|
(perms/make-check-fn has-read-permissions?))
|
|
|
|
;; --- QUERY: Get projects
|
|
|
|
(declare get-projects)
|
|
|
|
(s/def ::team-id ::us/uuid)
|
|
(s/def ::get-projects
|
|
(s/keys :req [::rpc/profile-id]
|
|
:req-un [::team-id]))
|
|
|
|
(sv/defmethod ::get-projects
|
|
{::doc/added "1.18"}
|
|
[{:keys [::db/pool]} {:keys [::rpc/profile-id team-id]}]
|
|
(dm/with-open [conn (db/open pool)]
|
|
(teams/check-read-permissions! conn profile-id team-id)
|
|
(get-projects conn profile-id team-id)))
|
|
|
|
(def sql:projects
|
|
"select p.*,
|
|
coalesce(tpp.is_pinned, false) as is_pinned,
|
|
(select count(*) from file as f
|
|
where f.project_id = p.id
|
|
and deleted_at is null) as count
|
|
from project as p
|
|
inner join team as t on (t.id = p.team_id)
|
|
left join team_project_profile_rel as tpp
|
|
on (tpp.project_id = p.id and
|
|
tpp.team_id = p.team_id and
|
|
tpp.profile_id = ?)
|
|
where p.team_id = ?
|
|
and p.deleted_at is null
|
|
and t.deleted_at is null
|
|
order by p.modified_at desc")
|
|
|
|
(defn get-projects
|
|
[conn profile-id team-id]
|
|
(db/exec! conn [sql:projects profile-id team-id]))
|
|
|
|
;; --- QUERY: Get all projects
|
|
|
|
(declare get-all-projects)
|
|
|
|
(s/def ::get-all-projects
|
|
(s/keys :req [::rpc/profile-id]))
|
|
|
|
(sv/defmethod ::get-all-projects
|
|
{::doc/added "1.18"}
|
|
[{:keys [::db/pool]} {:keys [::rpc/profile-id]}]
|
|
(dm/with-open [conn (db/open pool)]
|
|
(get-all-projects conn profile-id)))
|
|
|
|
(def sql:all-projects
|
|
"select p1.*, t.name as team_name, t.is_default as is_default_team
|
|
from project as p1
|
|
inner join team as t on (t.id = p1.team_id)
|
|
where t.id in (select team_id
|
|
from team_profile_rel as tpr
|
|
where tpr.profile_id = ?
|
|
and (tpr.can_edit = true or
|
|
tpr.is_owner = true or
|
|
tpr.is_admin = true))
|
|
and t.deleted_at is null
|
|
and p1.deleted_at is null
|
|
union
|
|
select p2.*, t.name as team_name, t.is_default as is_default_team
|
|
from project as p2
|
|
inner join team as t on (t.id = p2.team_id)
|
|
where p2.id in (select project_id
|
|
from project_profile_rel as ppr
|
|
where ppr.profile_id = ?
|
|
and (ppr.can_edit = true or
|
|
ppr.is_owner = true or
|
|
ppr.is_admin = true))
|
|
and t.deleted_at is null
|
|
and p2.deleted_at is null
|
|
order by team_name, name;")
|
|
|
|
(defn get-all-projects
|
|
[conn profile-id]
|
|
(db/exec! conn [sql:all-projects profile-id profile-id]))
|
|
|
|
|
|
;; --- QUERY: Get project
|
|
|
|
(s/def ::get-project
|
|
(s/keys :req [::rpc/profile-id]
|
|
:req-un [::id]))
|
|
|
|
(sv/defmethod ::get-project
|
|
{::doc/added "1.18"}
|
|
[{:keys [::db/pool]} {:keys [::rpc/profile-id id]}]
|
|
(dm/with-open [conn (db/open pool)]
|
|
(let [project (db/get-by-id conn :project id)]
|
|
(check-read-permissions! conn profile-id id)
|
|
project)))
|
|
|
|
|
|
|
|
;; --- MUTATION: Create Project
|
|
|
|
(s/def ::create-project
|
|
(s/keys :req [::rpc/profile-id]
|
|
:req-un [::team-id ::name]
|
|
:opt-un [::id]))
|
|
|
|
(sv/defmethod ::create-project
|
|
{::doc/added "1.18"
|
|
::webhooks/event? true}
|
|
[{: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})
|
|
|
|
(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))))
|
|
|
|
|
|
;; --- MUTATION: Toggle Project Pin
|
|
|
|
(def ^:private
|
|
sql:update-project-pin
|
|
"insert into team_project_profile_rel (team_id, project_id, profile_id, is_pinned)
|
|
values (?, ?, ?, ?)
|
|
on conflict (team_id, project_id, profile_id)
|
|
do update set is_pinned=?")
|
|
|
|
(s/def ::is-pinned ::us/boolean)
|
|
(s/def ::project-id ::us/uuid)
|
|
(s/def ::update-project-pin
|
|
(s/keys :req [::rpc/profile-id]
|
|
:req-un [::id ::team-id ::is-pinned]))
|
|
|
|
(sv/defmethod ::update-project-pin
|
|
{::doc/added "1.18"
|
|
::webhooks/batch-timeout (dt/duration "5s")
|
|
::webhooks/batch-key (webhooks/key-fn ::rpc/profile-id :id)
|
|
::webhooks/event? true}
|
|
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id team-id is-pinned] :as params}]
|
|
(db/with-atomic [conn pool]
|
|
(check-edition-permissions! conn profile-id id)
|
|
(db/exec-one! conn [sql:update-project-pin team-id id profile-id is-pinned is-pinned])
|
|
nil))
|
|
|
|
;; --- MUTATION: Rename Project
|
|
|
|
(declare rename-project)
|
|
|
|
(s/def ::rename-project
|
|
(s/keys :req [::rpc/profile-id]
|
|
:req-un [::name ::id]))
|
|
|
|
(sv/defmethod ::rename-project
|
|
{::doc/added "1.18"
|
|
::webhooks/event? true}
|
|
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id name] :as params}]
|
|
(db/with-atomic [conn pool]
|
|
(check-edition-permissions! conn profile-id id)
|
|
(let [project (db/get-by-id conn :project id ::sql/for-update true)]
|
|
(db/update! conn :project
|
|
{:name name}
|
|
{:id id})
|
|
(rph/with-meta (rph/wrap)
|
|
{::audit/props {:team-id (:team-id project)
|
|
:prev-name (:name project)}}))))
|
|
|
|
;; --- MUTATION: Delete Project
|
|
|
|
(s/def ::delete-project
|
|
(s/keys :req [::rpc/profile-id]
|
|
:req-un [::id]))
|
|
|
|
;; TODO: right now, we just don't allow delete default projects, in a
|
|
;; future we need to ensure raise a correct exception signaling that
|
|
;; this is not allowed.
|
|
|
|
(sv/defmethod ::delete-project
|
|
{::doc/added "1.18"
|
|
::webhooks/event? true}
|
|
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id] :as params}]
|
|
(db/with-atomic [conn pool]
|
|
(check-edition-permissions! conn profile-id id)
|
|
(let [project (db/update! conn :project
|
|
{:deleted-at (dt/now)}
|
|
{:id id :is-default false}
|
|
{::db/return-keys true})]
|
|
|
|
(wrk/submit! {::wrk/task :delete-object
|
|
::wrk/delay (dt/duration "1m")
|
|
::wrk/conn conn
|
|
:object :project
|
|
:deleted-at (:deleted-at project)
|
|
:id id})
|
|
|
|
(rph/with-meta (rph/wrap)
|
|
{::audit/props {:team-id (:team-id project)
|
|
:name (:name project)
|
|
:created-at (:created-at project)
|
|
:modified-at (:modified-at project)}}))))
|