mirror of
https://github.com/penpot/penpot.git
synced 2025-05-12 09:16:38 +02:00
✨ Integrate objects-map and introduce file feature flags
This commit is contained in:
parent
69f084e1df
commit
951b3eb4fe
21 changed files with 406 additions and 264 deletions
|
@ -352,10 +352,13 @@
|
|||
[v]
|
||||
(and (pgarray? v) (= "uuid" (.getBaseTypeName ^PgArray v))))
|
||||
|
||||
;; TODO rename to decode-pgarray-into
|
||||
(defn decode-pgarray
|
||||
([v] (some->> ^PgArray v .getArray vec))
|
||||
([v in] (some->> ^PgArray v .getArray (into in)))
|
||||
([v in xf] (some->> ^PgArray v .getArray (into in xf))))
|
||||
([v] (decode-pgarray v []))
|
||||
([v in]
|
||||
(into in (some-> ^PgArray v .getArray)))
|
||||
([v in xf]
|
||||
(into in xf (some-> ^PgArray v .getArray))))
|
||||
|
||||
(defn pgarray->set
|
||||
[v]
|
||||
|
|
|
@ -254,6 +254,9 @@
|
|||
{:name "0081-add-deleted-at-index-to-file-table"
|
||||
:fn (mg/resource "app/migrations/sql/0081-add-deleted-at-index-to-file-table.sql")}
|
||||
|
||||
{:name "0082-add-features-column-to-file-table"
|
||||
:fn (mg/resource "app/migrations/sql/0082-add-features-column-to-file-table.sql")}
|
||||
|
||||
])
|
||||
|
||||
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
ALTER TABLE file
|
||||
ADD COLUMN features text[] DEFAULT NULL;
|
|
@ -8,6 +8,8 @@
|
|||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.files.features :as ffeat]
|
||||
[app.common.logging :as l]
|
||||
[app.common.pages :as cp]
|
||||
[app.common.pages.migrations :as pmg]
|
||||
[app.common.spec :as us]
|
||||
|
@ -24,6 +26,7 @@
|
|||
[app.rpc.semaphore :as rsem]
|
||||
[app.storage.impl :as simpl]
|
||||
[app.util.blob :as blob]
|
||||
[app.util.objects-map :as omap]
|
||||
[app.util.services :as sv]
|
||||
[app.util.time :as dt]
|
||||
[clojure.spec.alpha :as s]
|
||||
|
@ -44,10 +47,11 @@
|
|||
|
||||
;; --- Mutation: Create File
|
||||
|
||||
(s/def ::features ::us/set-of-strings)
|
||||
(s/def ::is-shared ::us/boolean)
|
||||
(s/def ::create-file
|
||||
(s/keys :req-un [::profile-id ::name ::project-id]
|
||||
:opt-un [::id ::is-shared ::components-v2]))
|
||||
:opt-un [::id ::is-shared ::features ::components-v2]))
|
||||
|
||||
(sv/defmethod ::create-file
|
||||
[{:keys [pool] :as cfg} {:keys [profile-id project-id] :as params}]
|
||||
|
@ -68,27 +72,37 @@
|
|||
(defn create-file
|
||||
[conn {:keys [id name project-id is-shared data revn
|
||||
modified-at deleted-at ignore-sync-until
|
||||
components-v2]
|
||||
components-v2 features]
|
||||
:or {is-shared false revn 0}
|
||||
:as params}]
|
||||
(let [id (or id (:id data) (uuid/next))
|
||||
data (or data (ctf/make-file-data id components-v2))
|
||||
file (db/insert! conn :file
|
||||
(d/without-nils
|
||||
{:id id
|
||||
:project-id project-id
|
||||
:name name
|
||||
:revn revn
|
||||
:is-shared is-shared
|
||||
:data (blob/encode data)
|
||||
:ignore-sync-until ignore-sync-until
|
||||
:modified-at modified-at
|
||||
:deleted-at deleted-at}))]
|
||||
(let [id (or id (:id data) (uuid/next))
|
||||
|
||||
;; BACKWARD COMPATIBILITY with the components-v2 param
|
||||
features (cond-> (or features #{})
|
||||
components-v2 (conj "components/v2"))
|
||||
|
||||
data (or data
|
||||
(binding [ffeat/*current* features]
|
||||
(ctf/make-file-data id)))
|
||||
|
||||
features (db/create-array conn "text" features)
|
||||
file (db/insert! conn :file
|
||||
(d/without-nils
|
||||
{:id id
|
||||
:project-id project-id
|
||||
:name name
|
||||
:revn revn
|
||||
:is-shared is-shared
|
||||
:data (blob/encode data)
|
||||
:features features
|
||||
:ignore-sync-until ignore-sync-until
|
||||
:modified-at modified-at
|
||||
:deleted-at deleted-at}))]
|
||||
|
||||
(->> (assoc params :file-id id :role :owner)
|
||||
(create-file-role conn))
|
||||
|
||||
(assoc file :data data)))
|
||||
(-> file files/decode-row)))
|
||||
|
||||
;; --- Mutation: Rename File
|
||||
|
||||
|
@ -309,24 +323,59 @@
|
|||
(s/def ::update-file
|
||||
(s/and
|
||||
(s/keys :req-un [::id ::session-id ::profile-id ::revn]
|
||||
:opt-un [::changes ::changes-with-metadata ::components-v2])
|
||||
:opt-un [::changes ::changes-with-metadata ::components-v2 ::features])
|
||||
(fn [o]
|
||||
(or (contains? o :changes)
|
||||
(contains? o :changes-with-metadata)))))
|
||||
|
||||
(def ^:private sql:retrieve-file
|
||||
"SELECT f.*, p.team_id
|
||||
FROM file AS f
|
||||
JOIN project AS p ON (p.id = f.project_id)
|
||||
WHERE f.id = ?
|
||||
AND (f.deleted_at IS NULL OR
|
||||
f.deleted_at > now())
|
||||
FOR KEY SHARE")
|
||||
|
||||
(sv/defmethod ::update-file
|
||||
{::rsem/queue :update-file}
|
||||
[{:keys [pool] :as cfg} {:keys [id profile-id] :as params}]
|
||||
[{:keys [pool] :as cfg} {:keys [id profile-id components-v2] :as params}]
|
||||
(db/with-atomic [conn pool]
|
||||
(db/xact-lock! conn id)
|
||||
(let [{:keys [id] :as file} (db/get-by-id conn :file id {:for-key-share true})
|
||||
team-id (retrieve-team-id conn (:project-id file))]
|
||||
(files/check-edition-permissions! conn profile-id id)
|
||||
(with-meta
|
||||
(update-file (assoc cfg :conn conn)
|
||||
(assoc params :file file))
|
||||
{::audit/props {:project-id (:project-id file)
|
||||
:team-id team-id}}))))
|
||||
(let [file (db/exec-one! conn [sql:retrieve-file id])
|
||||
features' (:features params #{})
|
||||
features (db/decode-pgarray (:features file) features')
|
||||
|
||||
;; BACKWARD COMPATIBILITY with the components-v2 parameter
|
||||
features (cond-> features
|
||||
components-v2 (conj "components/v2"))
|
||||
|
||||
file (assoc file :features features)]
|
||||
|
||||
(when-not file
|
||||
(ex/raise :type :not-found
|
||||
:code :object-not-found
|
||||
:hint (format "file with id '%s' does not exists" id)))
|
||||
|
||||
;; If features are specified from params and the final feature
|
||||
;; set is different than the persisted one, update it on the
|
||||
;; database.
|
||||
(when (not= features features')
|
||||
(let [features (db/create-array conn "text" features)]
|
||||
(db/update! conn :file
|
||||
{:features features}
|
||||
{:id id})))
|
||||
|
||||
(binding [ffeat/*current* features
|
||||
ffeat/*wrap-objects-fn* (if (features "storate/objects-map")
|
||||
omap/wrap
|
||||
identity)]
|
||||
(files/check-edition-permissions! conn profile-id (:id file))
|
||||
(with-meta
|
||||
(update-file (assoc cfg :conn conn)
|
||||
(assoc params :file file))
|
||||
{::audit/props {:project-id (:project-id file)
|
||||
:team-id (:team-id file)}})))))
|
||||
|
||||
(defn- take-snapshot?
|
||||
"Defines the rule when file `data` snapshot should be saved."
|
||||
|
@ -347,7 +396,7 @@
|
|||
|
||||
(defn- update-file
|
||||
[{:keys [conn metrics] :as cfg}
|
||||
{:keys [file changes changes-with-metadata session-id profile-id components-v2] :as params}]
|
||||
{:keys [file changes changes-with-metadata session-id profile-id] :as params}]
|
||||
(when (> (:revn params)
|
||||
(:revn file))
|
||||
|
||||
|
@ -378,7 +427,8 @@
|
|||
(assoc :id (:id file))
|
||||
(pmg/migrate-data))
|
||||
|
||||
components-v2
|
||||
|
||||
(contains? ffeat/*current* "components/v2")
|
||||
(ctf/migrate-to-components-v2)
|
||||
|
||||
:always
|
||||
|
@ -455,7 +505,8 @@
|
|||
:changes changes})
|
||||
|
||||
(when (and (:is-shared file) (seq lchanges))
|
||||
(let [team-id (retrieve-team-id conn (:project-id file))]
|
||||
(let [team-id (or (:team-id file)
|
||||
(retrieve-team-id conn (:project-id file)))]
|
||||
;; Asynchronously publish message to the msgbus
|
||||
(mbus/pub! msgbus
|
||||
:topic team-id
|
||||
|
|
|
@ -227,29 +227,34 @@
|
|||
(d/index-by :object-id :data))))))
|
||||
|
||||
(defn retrieve-file
|
||||
[{:keys [pool] :as cfg} id components-v2]
|
||||
[{:keys [pool] :as cfg} id features]
|
||||
(let [file (->> (db/get-by-id pool :file id)
|
||||
(decode-row)
|
||||
(pmg/migrate-file))]
|
||||
|
||||
(if components-v2
|
||||
(if (contains? features "components/v2")
|
||||
(update file :data ctf/migrate-to-components-v2)
|
||||
(if (get-in file [:data :options :components-v2])
|
||||
(if (dm/get-in file [:data :options :components-v2])
|
||||
(ex/raise :type :restriction
|
||||
:code :feature-disabled
|
||||
:hint "tried to open a components-v2 file with feature disabled")
|
||||
:hint "tried to open a components/v2 file with feature disabled")
|
||||
file))))
|
||||
|
||||
(s/def ::features ::us/set-of-strings)
|
||||
(s/def ::file
|
||||
(s/keys :req-un [::profile-id ::id]
|
||||
:opt-un [::components-v2]))
|
||||
:opt-un [::features ::components-v2]))
|
||||
|
||||
(sv/defmethod ::file
|
||||
"Retrieve a file by its ID. Only authenticated users."
|
||||
[{:keys [pool] :as cfg} {:keys [profile-id id components-v2] :as params}]
|
||||
(let [perms (get-permissions pool profile-id id)]
|
||||
[{:keys [pool] :as cfg} {:keys [profile-id id features components-v2] :as params}]
|
||||
(let [perms (get-permissions pool profile-id id)
|
||||
|
||||
;; BACKWARD COMPATIBILTY with the components-v2 parameter
|
||||
features (cond-> (or features #{})
|
||||
components-v2 (conj features "components/v2"))]
|
||||
(check-read-permissions! perms)
|
||||
(let [file (retrieve-file cfg id components-v2)
|
||||
(let [file (retrieve-file cfg id features)
|
||||
thumbs (retrieve-object-thumbnails cfg id)]
|
||||
(-> file
|
||||
(assoc :thumbnails thumbs)
|
||||
|
@ -278,7 +283,7 @@
|
|||
(s/def ::page
|
||||
(s/and
|
||||
(s/keys :req-un [::profile-id ::file-id]
|
||||
:opt-un [::page-id ::object-id ::components-v2])
|
||||
:opt-un [::page-id ::object-id ::features ::components-v2])
|
||||
(fn [obj]
|
||||
(if (contains? obj :object-id)
|
||||
(contains? obj :page-id)
|
||||
|
@ -294,11 +299,15 @@
|
|||
mandatory.
|
||||
|
||||
Mainly used for rendering purposes."
|
||||
[{:keys [pool] :as cfg} {:keys [profile-id file-id page-id object-id components-v2] :as props}]
|
||||
[{:keys [pool] :as cfg} {:keys [profile-id file-id page-id object-id features components-v2] :as props}]
|
||||
(check-read-permissions! pool profile-id file-id)
|
||||
(let [file (retrieve-file cfg file-id components-v2)
|
||||
page-id (or page-id (-> file :data :pages first))
|
||||
page (get-in file [:data :pages-index page-id])]
|
||||
(let [;; BACKWARD COMPATIBILTY with the components-v2 parameter
|
||||
features (cond-> (or features #{})
|
||||
components-v2 (conj features "components/v2"))
|
||||
|
||||
file (retrieve-file cfg file-id features)
|
||||
page-id (or page-id (-> file :data :pages first))
|
||||
page (dm/get-in file [:data :pages-index page-id])]
|
||||
|
||||
(cond-> (prune-thumbnails page)
|
||||
(uuid? object-id)
|
||||
|
@ -384,14 +393,17 @@
|
|||
|
||||
(s/def ::file-data-for-thumbnail
|
||||
(s/keys :req-un [::profile-id ::file-id]
|
||||
:opt-un [::components-v2]))
|
||||
:opt-un [::components-v2 ::features]))
|
||||
|
||||
(sv/defmethod ::file-data-for-thumbnail
|
||||
"Retrieves the data for generate the thumbnail of the file. Used
|
||||
mainly for render thumbnails on dashboard."
|
||||
[{:keys [pool] :as cfg} {:keys [profile-id file-id components-v2] :as props}]
|
||||
[{:keys [pool] :as cfg} {:keys [profile-id file-id features components-v2] :as props}]
|
||||
(check-read-permissions! pool profile-id file-id)
|
||||
(let [file (retrieve-file cfg file-id components-v2)]
|
||||
(let [;; BACKWARD COMPATIBILTY with the components-v2 parameter
|
||||
features (cond-> (or features #{})
|
||||
components-v2 (conj features "components/v2"))
|
||||
file (retrieve-file cfg file-id features)]
|
||||
{:file-id file-id
|
||||
:revn (:revn file)
|
||||
:page (get-file-thumbnail-data cfg file)}))
|
||||
|
@ -567,8 +579,9 @@
|
|||
;; --- Helpers
|
||||
|
||||
(defn decode-row
|
||||
[{:keys [data changes] :as row}]
|
||||
[{:keys [data changes features] :as row}]
|
||||
(when row
|
||||
(cond-> row
|
||||
changes (assoc :changes (blob/decode changes))
|
||||
data (assoc :data (blob/decode data)))))
|
||||
features (assoc :features (db/decode-pgarray features))
|
||||
changes (assoc :changes (blob/decode changes))
|
||||
data (assoc :data (blob/decode data)))))
|
||||
|
|
|
@ -23,8 +23,8 @@
|
|||
(db/get-by-id pool :project id {:columns [:id :name :team-id]}))
|
||||
|
||||
(defn- retrieve-bundle
|
||||
[{:keys [pool] :as cfg} file-id profile-id components-v2]
|
||||
(p/let [file (files/retrieve-file cfg file-id components-v2)
|
||||
[{:keys [pool] :as cfg} file-id profile-id features]
|
||||
(p/let [file (files/retrieve-file cfg file-id features)
|
||||
project (retrieve-project pool (:project-id file))
|
||||
libs (files/retrieve-file-libraries cfg false file-id)
|
||||
users (comments/get-file-comments-users pool file-id profile-id)
|
||||
|
@ -45,40 +45,49 @@
|
|||
(s/def ::file-id ::us/uuid)
|
||||
(s/def ::profile-id ::us/uuid)
|
||||
(s/def ::share-id ::us/uuid)
|
||||
(s/def ::features ::us/set-of-strings)
|
||||
|
||||
;; TODO: deprecated, should be removed when version >= 1.18
|
||||
(s/def ::components-v2 ::us/boolean)
|
||||
|
||||
(s/def ::view-only-bundle
|
||||
(s/keys :req-un [::file-id] :opt-un [::profile-id ::share-id ::components-v2]))
|
||||
(s/keys :req-un [::file-id]
|
||||
:opt-un [::profile-id ::share-id ::features ::components-v2]))
|
||||
|
||||
(sv/defmethod ::view-only-bundle {:auth false}
|
||||
[{:keys [pool] :as cfg} {:keys [profile-id file-id share-id components-v2] :as params}]
|
||||
(p/let [slink (slnk/retrieve-share-link pool file-id share-id)
|
||||
[{:keys [pool] :as cfg} {:keys [profile-id file-id share-id features components-v2] :as params}]
|
||||
(p/let [;; BACKWARD COMPATIBILTY with the components-v2 parameter
|
||||
features (cond-> (or features #{})
|
||||
components-v2 (conj features "components/v2"))
|
||||
|
||||
slink (slnk/retrieve-share-link pool file-id share-id)
|
||||
perms (files/get-permissions pool profile-id file-id share-id)
|
||||
thumbs (files/retrieve-object-thumbnails cfg file-id)
|
||||
bundle (p/-> (retrieve-bundle cfg file-id profile-id components-v2)
|
||||
bundle (p/-> (retrieve-bundle cfg file-id profile-id features)
|
||||
(assoc :permissions perms)
|
||||
(assoc-in [:file :thumbnails] thumbs))]
|
||||
|
||||
;; When we have neither profile nor share, we just return a not
|
||||
;; found response to the user.
|
||||
(when (and (not profile-id)
|
||||
(not slink))
|
||||
(ex/raise :type :not-found
|
||||
:code :object-not-found))
|
||||
(do
|
||||
(when (and (not profile-id)
|
||||
(not slink))
|
||||
(ex/raise :type :not-found
|
||||
:code :object-not-found))
|
||||
|
||||
;; When we have only profile, we need to check read permissions
|
||||
;; on file.
|
||||
(when (and profile-id (not slink))
|
||||
(files/check-read-permissions! pool profile-id file-id))
|
||||
;; When we have only profile, we need to check read permissions
|
||||
;; on file.
|
||||
(when (and profile-id (not slink))
|
||||
(files/check-read-permissions! pool profile-id file-id))
|
||||
|
||||
(cond-> bundle
|
||||
(some? slink)
|
||||
(assoc :share slink)
|
||||
(cond-> bundle
|
||||
(some? slink)
|
||||
(assoc :share slink)
|
||||
|
||||
(and (some? slink)
|
||||
(and (some? slink)
|
||||
(not (contains? (:flags slink) "view-all-pages")))
|
||||
(update-in [:file :data] (fn [data]
|
||||
(let [allowed-pages (:pages slink)]
|
||||
(-> data
|
||||
(update :pages (fn [pages] (filterv #(contains? allowed-pages %) pages)))
|
||||
(update :pages-index (fn [index] (select-keys index allowed-pages))))))))))
|
||||
(update-in [:file :data] (fn [data]
|
||||
(let [allowed-pages (:pages slink)]
|
||||
(-> data
|
||||
(update :pages (fn [pages] (filterv #(contains? allowed-pages %) pages)))
|
||||
(update :pages-index (fn [index] (select-keys index allowed-pages)))))))))))
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue