Add nitrate endpoint to delete teams keeping your-penpot projects

This commit is contained in:
Pablo Alba
2026-03-18 12:31:39 +01:00
committed by Pablo Alba
parent ca72dcdcbb
commit 2a09f30199
2 changed files with 141 additions and 1 deletions

View File

@@ -339,6 +339,21 @@
;; --- COMMAND: Move project
(defn move-project
"Moves a project from one team to another.
Performs comprehensive validation including:
- Permission checks on both source and destination teams
- Team compatibility verification between source and destination
- File features compatibility with destination team
The operation also:
- Updates the project's team assignment
- Cleans up any broken library relations after the move
Throws:
- :cant-move-to-same-team if trying to move project to its current team
- Permission exceptions if user lacks required permissions
- Team compatibility exceptions if teams are incompatible"
[{:keys [::db/conn] :as cfg} {:keys [profile-id team-id project-id] :as params}]
(let [project (db/get-by-id conn :project project-id {:columns [:id :team-id]})
pids (->> (db/query conn :project {:team-id (:team-id project)} {:columns [:id]})

View File

@@ -8,6 +8,8 @@
"Internal Nitrate HTTP RPC API. Provides authenticated access to
organization management and token validation endpoints."
(:require
[app.common.data :as d]
[app.common.exceptions :as ex]
[app.common.features :as cfeat]
[app.common.schema :as sm]
[app.common.types.profile :refer [schema:profile, schema:basic-profile]]
@@ -18,12 +20,14 @@
[app.msgbus :as mbus]
[app.rpc :as-alias rpc]
[app.rpc.commands.files :as files]
[app.rpc.commands.management :as management]
[app.rpc.commands.profile :as profile]
[app.rpc.commands.teams :as teams]
[app.rpc.doc :as doc]
[app.rpc.quotes :as quotes]
[app.util.services :as sv]
[clojure.set :as set]))
[clojure.set :as set]
[cuerdas.core :as str]))
;; ---- API: authenticate
@@ -212,3 +216,124 @@
{:teams (mapv #(select-keys % [:id :name]) teams)
:num-files files-count})))))
;; ---- API: delete-teams-keeping-your-penpot-projects
(def ^:private sql:get-projects-and-default-teams
"Get projects from specified teams along with their team owner's default team information.
This query:
- Selects projects (id, team_id, name) from teams in the provided list
- Gets the profile_id of each team owner
- Gets the default_team_id where projects should be moved
- Only includes teams where the user is owner
- Only includes projects that contain at least one non-deleted file
- Excludes deleted projects and teams"
"SELECT p.id AS project_id,
p.team_id AS source_team_id,
p.name AS project_name,
tpr.profile_id,
pr.default_team_id
FROM project AS p
JOIN team AS tm ON p.team_id = tm.id
JOIN team_profile_rel AS tpr ON tm.id = tpr.team_id
JOIN profile AS pr ON tpr.profile_id = pr.id
WHERE p.team_id = ANY(?)
AND p.deleted_at IS NULL
AND tm.deleted_at IS NULL
AND tpr.is_owner IS TRUE
AND EXISTS (SELECT 1 FROM file f WHERE f.project_id = p.id AND f.deleted_at IS NULL);")
(def ^:private sql:delete-teams
"UPDATE team SET deleted_at = ? WHERE id = ANY(?)")
(def ^:private schema:delete-teams-keeping-your-penpot-projects
[:map
[:org-name ::sm/text]
[:teams [:vector [:map
[:id ::sm/uuid]
[:is-your-penpot ::sm/boolean]]]]])
(def ^:private schema:delete-teams-error
[:map
[:error ::sm/keyword]
[:message ::sm/text]
[:cause ::sm/text]
[:project-id {:optional true} ::sm/uuid]
[:project-name {:optional true} ::sm/text]
[:team-id {:optional true} ::sm/uuid]
[:phase {:optional true} [:enum :move-projects :delete-teams]]])
(def ^:private schema:delete-teams-result
[:or [:= nil] schema:delete-teams-error])
(defn- ^:private clean-org-name
"Clean and sanitize organization name to remove emojis, special characters,
and prevent potential injections. Only allows alphanumeric characters,
spaces, hyphens, underscores, and parentheses."
[org-name]
(when org-name
(-> org-name
str
str/trim
(str/replace #"[^\w\s\-_()]+" "")
(str/replace #"\s+" " ")
str/trim)))
(sv/defmethod ::delete-teams-keeping-your-penpot-projects
"For a list of teams, move the projects of your-penpot teams to the
default team of each team owner, then delete all provided teams."
{::doc/added "2.15"
::sm/params schema:delete-teams-keeping-your-penpot-projects
::sm/result schema:delete-teams-result}
[cfg {:keys [teams org-name ::rpc/request-at]}]
(let [your-penpot-team-ids (into [] (comp (filter :is-your-penpot) d/xf:map-id) teams)
all-team-ids (into [] d/xf:map-id teams)
cleaned-org-name (clean-org-name org-name)
org-prefix (if (str/empty? cleaned-org-name)
"imported: "
(str cleaned-org-name " imported: "))]
(when (seq all-team-ids)
(db/tx-run! cfg
(fn [{:keys [::db/conn] :as cfg}]
;; ---- Move projects ----
(when (seq your-penpot-team-ids)
(let [ids-array (db/create-array conn "uuid" your-penpot-team-ids)
projects (db/exec! conn [sql:get-projects-and-default-teams ids-array])]
(doseq [{:keys [default-team-id profile-id project-id project-name source-team-id]} projects
:when default-team-id]
(try
(management/move-project cfg {:profile-id profile-id
:team-id default-team-id
:project-id project-id})
(db/update! conn :project
{:is-default false
:name (str org-prefix project-name)}
{:id project-id})
(catch Throwable cause
(ex/raise :type :internal
:code :nitrate-project-move-failed
:context {:project-id project-id
:project-name project-name
:team-id source-team-id}
:cause cause))))))
;; ---- Delete teams ----
(try
(let [team-ids-array (db/create-array conn "uuid" all-team-ids)]
(db/exec-one! conn [sql:delete-teams request-at team-ids-array]))
(catch Throwable cause
(ex/raise :type :internal
:code :nitrate-team-deletion-failed
:context {:team-ids all-team-ids}
:cause cause))))))
nil))