mirror of
https://github.com/penpot/penpot.git
synced 2026-10-29 14:00:37 +00:00
🐛 Address review findings on circular import guard
F1 extracts the cycle check into bfc/library-link-status and reuses it in link-file-to-library and v3 import, keeping UI errors intact. F3 orders get-files-rels and manifest relations so the first edge wins deterministically, and documents it. F2 adds a three-node transitive cycle test; F4 asserts file_library_sync has no orphan rows for skipped links and matches rels otherwise. AI-assisted-by: muse-spark-1.3-contributor
This commit is contained in:
parent
48ab90b1ec
commit
291577035d
@ -473,7 +473,9 @@
|
||||
:deleted-at nil}))
|
||||
|
||||
(defn get-files-rels
|
||||
"Given a set of file-id's, return all matching relations with the libraries"
|
||||
"Given a set of file-id's, return all matching relations with the libraries.
|
||||
Rows come back in stable `(file-id, library-file-id)` order so export
|
||||
and import resolve cycles deterministically."
|
||||
[cfg ids]
|
||||
|
||||
(assert (set? ids) "expected a set of uuids")
|
||||
@ -490,7 +492,8 @@
|
||||
" ON fls.file_id = flr.file_id"
|
||||
" AND fls.library_file_id = flr.library_file_id"
|
||||
" WHERE flr.file_id = ANY(?)"
|
||||
" AND l.deleted_at IS NULL;")]
|
||||
" AND l.deleted_at IS NULL"
|
||||
" ORDER BY flr.file_id, flr.library_file_id;")]
|
||||
(db/exec! conn [sql ids])))))
|
||||
|
||||
(def ^:private sql:upsert-file-library-sync
|
||||
@ -526,6 +529,20 @@
|
||||
(->> (db/exec! conn [sql:get-libraries ids'])
|
||||
(into #{} xf-map-id))))))
|
||||
|
||||
(defn library-link-status
|
||||
"Check whether linking `file-id` to `library-id` is safe. Returns
|
||||
`:ok` when the link can be created, `:self-link` when both ids are
|
||||
the same, and `:circular` when the link would close a dependency
|
||||
cycle (i.e. `file-id` is already among the transitive libraries of
|
||||
`library-id`). Shared by the UI command (`link-file-to-library`,
|
||||
which raises) and the v3 importer (`import-file-relations`, which
|
||||
skips and logs), so both stay consistent."
|
||||
[cfg file-id library-id]
|
||||
(cond
|
||||
(= file-id library-id) :self-link
|
||||
(contains? (get-libraries cfg [library-id]) file-id) :circular
|
||||
:else :ok))
|
||||
|
||||
(defn get-file-object-thumbnails
|
||||
"Return all file object thumbnails for a given file."
|
||||
[cfg file-id]
|
||||
|
||||
@ -986,37 +986,45 @@
|
||||
file-id')))
|
||||
|
||||
(defn- import-file-relations
|
||||
"Import library relations from the manifest, skipping edges that would
|
||||
close a dependency cycle or link a file to itself (same check as
|
||||
`link-file-to-library`, see `bfc/library-link-status`). Relations are
|
||||
processed in sorted manifest order and the first edge wins: an edge
|
||||
that would close a cycle with previously imported relations is skipped
|
||||
and logged."
|
||||
[{:keys [::db/conn ::manifest ::bfc/timestamp] :as cfg}]
|
||||
(events/tap :progress {:section :relations})
|
||||
(doseq [[file-id libr-id] (:relations manifest)]
|
||||
(let [relations (sort-by (fn [[file-id libr-id]] [(str file-id) (str libr-id)])
|
||||
(:relations manifest))]
|
||||
(doseq [[file-id libr-id] relations]
|
||||
|
||||
(let [file-id (bfc/lookup-index file-id)
|
||||
libr-id (bfc/lookup-index libr-id)]
|
||||
(let [file-id (bfc/lookup-index file-id)
|
||||
libr-id (bfc/lookup-index libr-id)]
|
||||
|
||||
(when (and file-id libr-id)
|
||||
(cond
|
||||
(= file-id libr-id)
|
||||
(l/warn :hint "ignoring self-referencing library link"
|
||||
:file-id (str file-id)
|
||||
:lib-id (str libr-id)
|
||||
::l/sync? true)
|
||||
(when (and file-id libr-id)
|
||||
(case (bfc/library-link-status cfg file-id libr-id)
|
||||
:self-link
|
||||
(l/warn :hint "ignoring self-referencing library link"
|
||||
:file-id (str file-id)
|
||||
:lib-id (str libr-id)
|
||||
::l/sync? true)
|
||||
|
||||
(contains? (bfc/get-libraries cfg [libr-id]) file-id)
|
||||
(l/warn :hint "ignoring circular library link"
|
||||
:file-id (str file-id)
|
||||
:lib-id (str libr-id)
|
||||
::l/sync? true)
|
||||
:circular
|
||||
(l/warn :hint "ignoring circular library link"
|
||||
:file-id (str file-id)
|
||||
:lib-id (str libr-id)
|
||||
::l/sync? true)
|
||||
|
||||
:else
|
||||
(do
|
||||
(l/dbg :hint "create file library link"
|
||||
:file-id (str file-id)
|
||||
:lib-id (str libr-id)
|
||||
::l/sync? true)
|
||||
(let [rel-params {:file-id file-id
|
||||
:library-file-id libr-id}]
|
||||
(db/insert! conn :file-library-rel rel-params)
|
||||
(bfc/upsert-file-library-sync! conn (assoc rel-params :synced-at timestamp)))))))))
|
||||
:ok
|
||||
(do
|
||||
(l/dbg :hint "create file library link"
|
||||
:file-id (str file-id)
|
||||
:lib-id (str libr-id)
|
||||
::l/sync? true)
|
||||
(let [rel-params {:file-id file-id
|
||||
:library-file-id libr-id}]
|
||||
(db/insert! conn :file-library-rel rel-params)
|
||||
(bfc/upsert-file-library-sync! conn (assoc rel-params :synced-at timestamp))))))))))
|
||||
|
||||
(defn- import-storage-objects
|
||||
[{:keys [::bfc/input ::entries-index ::bfc/timestamp] :as cfg}]
|
||||
|
||||
@ -1173,21 +1173,23 @@
|
||||
::db/transaction true}
|
||||
[{:keys [::db/conn] :as cfg} {:keys [::rpc/profile-id file-id library-id] :as params}]
|
||||
|
||||
(when (= file-id library-id)
|
||||
(case (bfc/library-link-status cfg file-id library-id)
|
||||
:self-link
|
||||
(ex/raise :type :validation
|
||||
:code :invalid-library
|
||||
:hint "A file cannot be linked to itself"))
|
||||
:hint "A file cannot be linked to itself")
|
||||
|
||||
:circular
|
||||
(ex/raise :type :validation
|
||||
:code :circular-library-reference
|
||||
:hint "linking this library would create a circular dependency")
|
||||
|
||||
:ok nil)
|
||||
|
||||
(check-edition-permissions! conn profile-id file-id)
|
||||
(check-edition-permissions! conn profile-id library-id)
|
||||
(check-library-team-ownership! conn file-id library-id)
|
||||
|
||||
(let [transitive-deps (bfc/get-libraries cfg [library-id])]
|
||||
(when (contains? transitive-deps file-id)
|
||||
(ex/raise :type :validation
|
||||
:code :circular-library-reference
|
||||
:hint "linking this library would create a circular dependency")))
|
||||
|
||||
(link-file-to-library conn params)
|
||||
(bfc/get-libraries cfg [library-id]))
|
||||
|
||||
|
||||
@ -2900,7 +2900,8 @@
|
||||
;; each other (possible before the 2.17.1 guard) export both relations in
|
||||
;; the manifest; the import must skip the one closing the cycle instead
|
||||
;; of recreating it. The cycle is built with direct inserts because the
|
||||
;; UI command `link-file-to-library` already rejects it.
|
||||
;; UI command `link-file-to-library` already rejects it. Relations are
|
||||
;; processed in sorted manifest order and the first edge wins.
|
||||
(let [profile (th/create-profile* 1)
|
||||
lib-a (th/create-file* 1 {:profile-id (:id profile)
|
||||
:project-id (:default-project-id profile)
|
||||
@ -2930,10 +2931,71 @@
|
||||
(assoc ::bfc/input output)
|
||||
(v3/import-files!)))
|
||||
rels (mapcat #(db/query th/*system* :file-library-rel {:file-id %})
|
||||
result)
|
||||
syncs (mapcat #(db/query th/*system* :file-library-sync {:file-id %})
|
||||
result)]
|
||||
(t/is (= 2 (count result)))
|
||||
;; only one of the two cycle edges survives the import
|
||||
(t/is (= 1 (count rels)))
|
||||
;; the skipped edge leaves no sync row behind
|
||||
(t/is (= 1 (count syncs)))
|
||||
(t/is (= (set (map (juxt :file-id :library-file-id) syncs))
|
||||
(set (map (juxt :file-id :library-file-id) rels))))
|
||||
;; no imported file lists itself among its transitive libraries
|
||||
(doseq [file-id result]
|
||||
(t/is (not (contains? (bfc/get-libraries th/*system* [file-id])
|
||||
file-id))))))))
|
||||
|
||||
(t/deftest import-skips-transitive-cycle
|
||||
;; A three-node cycle (A -> B -> C -> A) only breaks when the check uses
|
||||
;; the transitive closure: a direct-edge-only check would keep all three
|
||||
;; edges. Built with direct inserts because the UI command rejects it.
|
||||
(let [profile (th/create-profile* 1)
|
||||
lib-a (th/create-file* 1 {:profile-id (:id profile)
|
||||
:project-id (:default-project-id profile)
|
||||
:is-shared true
|
||||
:name "Library A"})
|
||||
lib-b (th/create-file* 2 {:profile-id (:id profile)
|
||||
:project-id (:default-project-id profile)
|
||||
:is-shared true
|
||||
:name "Library B"})
|
||||
lib-c (th/create-file* 3 {:profile-id (:id profile)
|
||||
:project-id (:default-project-id profile)
|
||||
:is-shared true
|
||||
:name "Library C"})]
|
||||
(db/insert! th/*system* :file-library-rel
|
||||
{:file-id (:id lib-a)
|
||||
:library-file-id (:id lib-b)})
|
||||
(db/insert! th/*system* :file-library-rel
|
||||
{:file-id (:id lib-b)
|
||||
:library-file-id (:id lib-c)})
|
||||
(db/insert! th/*system* :file-library-rel
|
||||
{:file-id (:id lib-c)
|
||||
:library-file-id (:id lib-a)})
|
||||
|
||||
(let [output (tmp/tempfile :suffix ".zip")]
|
||||
(v3/export-files!
|
||||
(-> th/*system*
|
||||
(assoc ::bfc/ids #{(:id lib-a) (:id lib-b) (:id lib-c)})
|
||||
(assoc ::bfc/export-type :include-libraries))
|
||||
(io/output-stream output))
|
||||
|
||||
(let [result (:file-ids (-> th/*system*
|
||||
(assoc ::bfc/project-id (:default-project-id profile))
|
||||
(assoc ::bfc/profile-id (:id profile))
|
||||
(assoc ::bfc/input output)
|
||||
(v3/import-files!)))
|
||||
rels (mapcat #(db/query th/*system* :file-library-rel {:file-id %})
|
||||
result)
|
||||
syncs (mapcat #(db/query th/*system* :file-library-sync {:file-id %})
|
||||
result)]
|
||||
(t/is (= 3 (count result)))
|
||||
;; only two of the three cycle edges survive the import
|
||||
(t/is (= 2 (count rels)))
|
||||
;; the skipped edge leaves no sync row behind
|
||||
(t/is (= 2 (count syncs)))
|
||||
(t/is (= (set (map (juxt :file-id :library-file-id) syncs))
|
||||
(set (map (juxt :file-id :library-file-id) rels))))
|
||||
;; no imported file lists itself among its transitive libraries
|
||||
(doseq [file-id result]
|
||||
(t/is (not (contains? (bfc/get-libraries th/*system* [file-id])
|
||||
@ -2964,9 +3026,13 @@
|
||||
(assoc ::bfc/input output)
|
||||
(v3/import-files!)))
|
||||
rels (mapcat #(db/query th/*system* :file-library-rel {:file-id %})
|
||||
result)
|
||||
syncs (mapcat #(db/query th/*system* :file-library-sync {:file-id %})
|
||||
result)]
|
||||
(t/is (= 1 (count result)))
|
||||
(t/is (= 0 (count rels)))))))
|
||||
(t/is (= 0 (count rels)))
|
||||
;; the skipped self-link leaves no sync row behind
|
||||
(t/is (= 0 (count syncs)))))))
|
||||
|
||||
(t/deftest import-keeps-acyclic-library-relations
|
||||
;; Control test: a chain without cycles must be imported untouched, so
|
||||
@ -3004,6 +3070,12 @@
|
||||
(assoc ::bfc/input output)
|
||||
(v3/import-files!)))
|
||||
rels (mapcat #(db/query th/*system* :file-library-rel {:file-id %})
|
||||
result)
|
||||
syncs (mapcat #(db/query th/*system* :file-library-sync {:file-id %})
|
||||
result)]
|
||||
(t/is (= 3 (count result)))
|
||||
(t/is (= 2 (count rels)))))))
|
||||
(t/is (= 2 (count rels)))
|
||||
;; every kept relation has its sync row
|
||||
(t/is (= 2 (count syncs)))
|
||||
(t/is (= (set (map (juxt :file-id :library-file-id) syncs))
|
||||
(set (map (juxt :file-id :library-file-id) rels))))))))
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user