mirror of
https://github.com/penpot/penpot.git
synced 2025-05-01 23:26:20 +02:00
this allows almost all api operations to success usin application/json encoding with the exception of the update-file, which we need to approach a bit differently; the reason update-file is different, is because the operations vector is right now defined without the context of shape type, so we are just unable to properly parse the value to correct type using the schema decoding mechanism
454 lines
18 KiB
Clojure
454 lines
18 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.management
|
|
"A collection of RPC methods for manage the files, projects and team organization."
|
|
(:require
|
|
[app.binfile.common :as bfc]
|
|
[app.binfile.v1 :as bf.v1]
|
|
[app.common.exceptions :as ex]
|
|
[app.common.features :as cfeat]
|
|
[app.common.schema :as sm]
|
|
[app.common.uuid :as uuid]
|
|
[app.config :as cf]
|
|
[app.db :as db]
|
|
[app.http.sse :as sse]
|
|
[app.loggers.audit :as audit]
|
|
[app.loggers.webhooks :as-alias webhooks]
|
|
[app.rpc :as-alias rpc]
|
|
[app.rpc.commands.files :as files]
|
|
[app.rpc.commands.projects :as proj]
|
|
[app.rpc.commands.teams :as teams]
|
|
[app.rpc.doc :as-alias doc]
|
|
[app.setup :as-alias setup]
|
|
[app.setup.templates :as tmpl]
|
|
[app.util.services :as sv]
|
|
[app.util.time :as dt]
|
|
[app.worker :as-alias wrk]
|
|
[promesa.exec :as px]))
|
|
|
|
;; --- COMMAND: Duplicate File
|
|
|
|
(defn duplicate-file
|
|
[{:keys [::db/conn ::bfc/timestamp] :as cfg} {:keys [profile-id file-id name reset-shared-flag] :as params}]
|
|
(let [;; We don't touch the original file on duplication
|
|
file (bfc/get-file cfg file-id)
|
|
project-id (:project-id file)
|
|
file (-> file
|
|
(update :id bfc/lookup-index)
|
|
(update :project-id bfc/lookup-index)
|
|
(cond-> (string? name)
|
|
(assoc :name name))
|
|
(cond-> (true? reset-shared-flag)
|
|
(assoc :is-shared false)))
|
|
|
|
flibs (bfc/get-files-rels cfg #{file-id})
|
|
fmeds (bfc/get-file-media cfg file)]
|
|
|
|
(when (uuid? profile-id)
|
|
(proj/check-edition-permissions! conn profile-id project-id))
|
|
|
|
(vswap! bfc/*state* update :index bfc/update-index fmeds :id)
|
|
|
|
;; Process and persist file
|
|
(let [file (->> (bfc/process-file file)
|
|
(bfc/persist-file! cfg))]
|
|
|
|
;; The file profile creation is optional, so when no profile is
|
|
;; present (when this function is called from profile less
|
|
;; environment: SREPL) we just omit the creation of the relation
|
|
(when (uuid? profile-id)
|
|
(db/insert! conn :file-profile-rel
|
|
{:file-id (:id file)
|
|
:profile-id profile-id
|
|
:is-owner true
|
|
:is-admin true
|
|
:can-edit true}
|
|
{::db/return-keys? false}))
|
|
|
|
(doseq [params (sequence (comp
|
|
(map #(bfc/remap-id % :file-id))
|
|
(map #(bfc/remap-id % :library-file-id))
|
|
(map #(assoc % :synced-at timestamp))
|
|
(map #(assoc % :created-at timestamp)))
|
|
flibs)]
|
|
(db/insert! conn :file-library-rel params ::db/return-keys false))
|
|
|
|
(doseq [params (sequence (comp
|
|
(map #(bfc/remap-id % :id))
|
|
(map #(assoc % :created-at timestamp))
|
|
(map #(bfc/remap-id % :file-id)))
|
|
fmeds)]
|
|
(db/insert! conn :file-media-object params ::db/return-keys false))
|
|
|
|
file)))
|
|
|
|
(def ^:private
|
|
schema:duplicate-file
|
|
(sm/define
|
|
[:map {:title "duplicate-file"}
|
|
[:file-id ::sm/uuid]
|
|
[:name {:optional true} [:string {:max 250}]]]))
|
|
|
|
(sv/defmethod ::duplicate-file
|
|
"Duplicate a single file in the same team."
|
|
{::doc/added "1.16"
|
|
::webhooks/event? true
|
|
::sm/params schema:duplicate-file}
|
|
[cfg {:keys [::rpc/profile-id file-id] :as params}]
|
|
(db/tx-run! cfg (fn [{:keys [::db/conn] :as cfg}]
|
|
(db/exec-one! conn ["SET CONSTRAINTS ALL DEFERRED"])
|
|
|
|
(binding [bfc/*state* (volatile! {:index {file-id (uuid/next)}})]
|
|
(duplicate-file (assoc cfg ::bfc/timestamp (dt/now))
|
|
(-> params
|
|
(assoc :profile-id profile-id)
|
|
(assoc :reset-shared-flag true)))))))
|
|
|
|
;; --- COMMAND: Duplicate Project
|
|
|
|
(defn duplicate-project
|
|
[{:keys [::db/conn ::bfc/timestamp] :as cfg} {:keys [profile-id project-id name] :as params}]
|
|
(binding [bfc/*state* (volatile! {:index {project-id (uuid/next)}})]
|
|
(let [project (-> (db/get-by-id conn :project project-id)
|
|
(assoc :created-at timestamp)
|
|
(assoc :modified-at timestamp)
|
|
(assoc :is-pinned false)
|
|
(update :id bfc/lookup-index)
|
|
(cond-> (string? name)
|
|
(assoc :name name)))
|
|
|
|
files (bfc/get-project-files cfg project-id)]
|
|
|
|
;; Update index with the project files and the project-id
|
|
(vswap! bfc/*state* update :index bfc/update-index files)
|
|
|
|
|
|
;; Check if the source team-id allow creating new project for current user
|
|
(teams/check-edition-permissions! conn profile-id (:team-id project))
|
|
|
|
;; create the duplicated project and assign the current profile as
|
|
;; a project owner
|
|
(let [project (teams/create-project conn project)]
|
|
;; The project profile creation is optional, so when no profile is
|
|
;; present (when this function is called from profile less
|
|
;; environment: SREPL) we just omit the creation of the relation
|
|
(when (uuid? profile-id)
|
|
(teams/create-project-role conn profile-id (:id project) :owner))
|
|
|
|
(doseq [file-id files]
|
|
(let [params (-> params
|
|
(dissoc :name)
|
|
(assoc :file-id file-id)
|
|
(assoc :reset-shared-flag false))]
|
|
(duplicate-file cfg params)))
|
|
|
|
project))))
|
|
|
|
(def ^:private
|
|
schema:duplicate-project
|
|
(sm/define
|
|
[:map {:title "duplicate-project"}
|
|
[:project-id ::sm/uuid]
|
|
[:name {:optional true} [:string {:max 250}]]]))
|
|
|
|
(sv/defmethod ::duplicate-project
|
|
"Duplicate an entire project with all the files"
|
|
{::doc/added "1.16"
|
|
::webhooks/event? true
|
|
::sm/params schema:duplicate-project}
|
|
[cfg {:keys [::rpc/profile-id] :as params}]
|
|
(db/tx-run! cfg (fn [cfg]
|
|
;; Defer all constraints
|
|
(db/exec-one! cfg ["SET CONSTRAINTS ALL DEFERRED"])
|
|
(-> (assoc cfg ::bfc/timestamp (dt/now))
|
|
(duplicate-project (assoc params :profile-id profile-id))))))
|
|
|
|
(defn duplicate-team
|
|
[{:keys [::db/conn ::bfc/timestamp] :as cfg} & {:keys [profile-id team-id name] :as params}]
|
|
|
|
;; Check if the source team-id allowed to be read by the user if
|
|
;; profile-id is present; it can be ommited if this function is
|
|
;; called from SREPL helpers where no profile is available
|
|
(when (uuid? profile-id)
|
|
(teams/check-read-permissions! conn profile-id team-id))
|
|
|
|
(binding [bfc/*state* (volatile! {:index {team-id (uuid/next)}})]
|
|
(let [projs (bfc/get-team-projects cfg team-id)
|
|
files (bfc/get-team-files cfg team-id)
|
|
frels (bfc/get-files-rels cfg files)
|
|
|
|
team (-> (db/get-by-id conn :team team-id)
|
|
(assoc :created-at timestamp)
|
|
(assoc :modified-at timestamp)
|
|
(update :id bfc/lookup-index)
|
|
(cond-> (string? name)
|
|
(assoc :name name)))
|
|
|
|
fonts (db/query conn :team-font-variant
|
|
{:team-id team-id})]
|
|
|
|
(vswap! bfc/*state* update :index
|
|
(fn [index]
|
|
(-> index
|
|
(bfc/update-index projs)
|
|
(bfc/update-index files)
|
|
(bfc/update-index fonts :id))))
|
|
|
|
;; FIXME: disallow clone default team
|
|
;; Create the new team in the database
|
|
(db/insert! conn :team team)
|
|
|
|
;; Duplicate team <-> profile relations
|
|
(doseq [params frels]
|
|
(let [params (-> params
|
|
(assoc :id (uuid/next))
|
|
(update :team-id bfc/lookup-index)
|
|
(assoc :created-at timestamp)
|
|
(assoc :modified-at timestamp))]
|
|
(db/insert! conn :team-profile-rel params
|
|
{::db/return-keys false})))
|
|
|
|
;; Duplicate team fonts
|
|
(doseq [font fonts]
|
|
(let [params (-> font
|
|
(update :id bfc/lookup-index)
|
|
(update :team-id bfc/lookup-index)
|
|
(assoc :created-at timestamp)
|
|
(assoc :modified-at timestamp))]
|
|
(db/insert! conn :team-font-variant params
|
|
{::db/return-keys false})))
|
|
|
|
;; Duplicate projects; We don't reuse the `duplicate-project`
|
|
;; here because we handle files duplication by whole team
|
|
;; instead of by project and we want to preserve some project
|
|
;; props which are reset on the `duplicate-project` impl
|
|
(doseq [project-id projs]
|
|
(let [project (db/get conn :project {:id project-id})
|
|
project (-> project
|
|
(assoc :created-at timestamp)
|
|
(assoc :modified-at timestamp)
|
|
(update :id bfc/lookup-index)
|
|
(update :team-id bfc/lookup-index))]
|
|
(teams/create-project conn project)
|
|
|
|
;; The project profile creation is optional, so when no profile is
|
|
;; present (when this function is called from profile less
|
|
;; environment: SREPL) we just omit the creation of the relation
|
|
(when (uuid? profile-id)
|
|
(teams/create-project-role conn profile-id (:id project) :owner))))
|
|
|
|
(doseq [file-id files]
|
|
(let [params (-> params
|
|
(dissoc :name)
|
|
(assoc :file-id file-id)
|
|
(assoc :reset-shared-flag false))]
|
|
(duplicate-file cfg params)))
|
|
|
|
team)))
|
|
|
|
;; --- COMMAND: Move file
|
|
|
|
(def sql:get-files
|
|
"select id, features, project_id from file where id = ANY(?)")
|
|
|
|
(def sql:move-files
|
|
"update file set project_id = ? where id = ANY(?)")
|
|
|
|
(def sql:delete-broken-relations
|
|
"with broken as (
|
|
(select * from file_library_rel as flr
|
|
inner join file as f on (flr.file_id = f.id)
|
|
inner join project as p on (f.project_id = p.id)
|
|
inner join file as lf on (flr.library_file_id = lf.id)
|
|
inner join project as lp on (lf.project_id = lp.id)
|
|
where p.id = ANY(?)
|
|
and lp.team_id != p.team_id)
|
|
)
|
|
delete from file_library_rel as rel
|
|
using broken as br
|
|
where rel.file_id = br.file_id
|
|
and rel.library_file_id = br.library_file_id")
|
|
|
|
(defn move-files
|
|
[{:keys [::db/conn] :as cfg} {:keys [profile-id ids project-id] :as params}]
|
|
|
|
(let [fids (db/create-array conn "uuid" ids)
|
|
files (->> (db/exec! conn [sql:get-files fids])
|
|
(map files/decode-row))
|
|
source (into #{} (map :project-id) files)
|
|
pids (->> (conj source project-id)
|
|
(db/create-array conn "uuid"))]
|
|
|
|
(when (contains? source project-id)
|
|
(ex/raise :type :validation
|
|
:code :cant-move-to-same-project
|
|
:hint "Unable to move a file to the same project"))
|
|
|
|
;; Check if we have permissions on the destination project
|
|
(proj/check-edition-permissions! conn profile-id project-id)
|
|
|
|
;; Check if we have permissions on all source projects
|
|
(doseq [project-id source]
|
|
(proj/check-edition-permissions! conn profile-id project-id))
|
|
|
|
;; Check the team compatibility
|
|
(let [orig-team (teams/get-team conn :profile-id profile-id :project-id (first source))
|
|
dest-team (teams/get-team conn :profile-id profile-id :project-id project-id)]
|
|
(cfeat/check-teams-compatibility! orig-team dest-team)
|
|
|
|
;; Check if all pending to move files are compaib
|
|
(let [features (cfeat/get-team-enabled-features cf/flags dest-team)]
|
|
(doseq [file files]
|
|
(cfeat/check-file-features! features (:features file)))))
|
|
|
|
;; move all files to the project
|
|
(db/exec-one! conn [sql:move-files project-id fids])
|
|
|
|
;; delete possible broken relations on moved files
|
|
(db/exec-one! conn [sql:delete-broken-relations pids])
|
|
|
|
;; Update the modification date of the all affected projects
|
|
;; ensuring that the destination project is the most recent one.
|
|
(doseq [project-id (into (list project-id) source)]
|
|
|
|
;; NOTE: as this is executed on virtual thread, sleeping does
|
|
;; not causes major issues, and allows an easy way to set a
|
|
;; trully different modification date to each file.
|
|
(px/sleep 10)
|
|
(db/update! conn :project
|
|
{:modified-at (dt/now)}
|
|
{:id project-id}))
|
|
|
|
nil))
|
|
|
|
(def ^:private
|
|
schema:move-files
|
|
(sm/define
|
|
[:map {:title "move-files"}
|
|
[:ids ::sm/set-of-uuid]
|
|
[:project-id ::sm/uuid]]))
|
|
|
|
(sv/defmethod ::move-files
|
|
"Move a set of files from one project to other."
|
|
{::doc/added "1.16"
|
|
::webhooks/event? true
|
|
::sm/params schema:move-files}
|
|
[cfg {:keys [::rpc/profile-id] :as params}]
|
|
(db/tx-run! cfg #(move-files % (assoc params :profile-id profile-id))))
|
|
|
|
;; --- COMMAND: Move project
|
|
|
|
(defn move-project
|
|
[{: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]})
|
|
(map :id)
|
|
(db/create-array conn "uuid"))]
|
|
|
|
(when (= team-id (:team-id project))
|
|
(ex/raise :type :validation
|
|
:code :cant-move-to-same-team
|
|
:hint "Unable to move a project to same team"))
|
|
|
|
(teams/check-edition-permissions! conn profile-id (:team-id project))
|
|
(teams/check-edition-permissions! conn profile-id team-id)
|
|
|
|
;; Check the teams compatibility
|
|
(let [orig-team (teams/get-team conn :profile-id profile-id :team-id (:team-id project))
|
|
dest-team (teams/get-team conn :profile-id profile-id :team-id team-id)]
|
|
(cfeat/check-teams-compatibility! orig-team dest-team)
|
|
|
|
;; Check if all pending to move files are compaib
|
|
(let [features (cfeat/get-team-enabled-features cf/flags dest-team)]
|
|
(doseq [file (->> (db/query conn :file
|
|
{:project-id project-id}
|
|
{:columns [:features]})
|
|
(map files/decode-row))]
|
|
(cfeat/check-file-features! features (:features file)))))
|
|
|
|
;; move project to the destination team
|
|
(db/update! conn :project
|
|
{:team-id team-id}
|
|
{:id project-id})
|
|
|
|
;; delete possible broken relations on moved files
|
|
(db/exec-one! conn [sql:delete-broken-relations pids])
|
|
|
|
nil))
|
|
|
|
(def ^:private
|
|
schema:move-project
|
|
[:map {:title "move-project"}
|
|
[:team-id ::sm/uuid]
|
|
[:project-id ::sm/uuid]])
|
|
|
|
(sv/defmethod ::move-project
|
|
"Move projects between teams"
|
|
{::doc/added "1.16"
|
|
::webhooks/event? true
|
|
::sm/params schema:move-project}
|
|
[cfg {:keys [::rpc/profile-id] :as params}]
|
|
(db/tx-run! cfg #(move-project % (assoc params :profile-id profile-id))))
|
|
|
|
;; --- COMMAND: Clone Template
|
|
|
|
(defn- clone-template
|
|
[cfg {:keys [project-id ::rpc/profile-id] :as params} template]
|
|
(db/tx-run! cfg (fn [{:keys [::db/conn ::wrk/executor] :as cfg}]
|
|
;; NOTE: the importation process performs some operations that
|
|
;; are not very friendly with virtual threads, and for avoid
|
|
;; unexpected blocking of other concurrent operations we
|
|
;; dispatch that operation to a dedicated executor.
|
|
(let [cfg (-> cfg
|
|
(assoc ::bf.v1/project-id project-id)
|
|
(assoc ::bf.v1/profile-id profile-id))
|
|
result (px/invoke! executor (partial bf.v1/import-files! cfg template))]
|
|
|
|
(db/update! conn :project
|
|
{:modified-at (dt/now)}
|
|
{:id project-id})
|
|
|
|
(let [props (audit/clean-props params)]
|
|
(doseq [file-id result]
|
|
(let [props (assoc props :id file-id)
|
|
event (-> (audit/event-from-rpc-params params)
|
|
(assoc ::audit/name "create-file")
|
|
(assoc ::audit/props props))]
|
|
(audit/submit! cfg event))))
|
|
|
|
result))))
|
|
|
|
(def ^:private
|
|
schema:clone-template
|
|
[:map {:title "clone-template"}
|
|
[:project-id ::sm/uuid]
|
|
[:template-id ::sm/word-string]])
|
|
|
|
(sv/defmethod ::clone-template
|
|
"Clone into the specified project the template by its id."
|
|
{::doc/added "1.16"
|
|
::sse/stream? true
|
|
::webhooks/event? true
|
|
::sm/params schema:clone-template}
|
|
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id project-id template-id] :as params}]
|
|
(let [project (db/get-by-id pool :project project-id {:columns [:id :team-id]})
|
|
_ (teams/check-edition-permissions! pool profile-id (:team-id project))
|
|
template (tmpl/get-template-stream cfg template-id)]
|
|
|
|
(when-not template
|
|
(ex/raise :type :not-found
|
|
:code :template-not-found
|
|
:hint "template not found"))
|
|
|
|
(sse/response #(clone-template cfg params template))))
|
|
|
|
;; --- COMMAND: Get list of builtin templates
|
|
|
|
(sv/defmethod ::get-builtin-templates
|
|
{::doc/added "1.19"}
|
|
[cfg _params]
|
|
(mapv #(select-keys % [:id :name]) (::setup/templates cfg)))
|