🐛 Keep svg-raw children as uuids on binfile import (#10837)

Importing a .penpot file left every svg-raw subtree broken: the parent's
:shapes vector came back holding plain strings instead of uuids, so the
child ids no longer resolved against the page objects map. The next
persisted change touching that page then failed referential integrity
validation with :child-not-found, surfaced to the client as an HTTP 400
:referential-integrity error, which in practice bricks the file.

An svg-raw shape can be a container: importing an SVG builds a tree of
svg-raw shapes, and cfh/group-like-shape? explicitly treats an svg-raw
with children as group-like. But schema:svg-raw-attrs was an empty map.
Frame, group and bool all declare :shapes as a vector of uuid; svg-raw
did not, so the JSON decoder used by binfile had no type information for
those ids and left them as strings.

Declare :shapes on schema:svg-raw-attrs, optional because a leaf svg-raw
shape has no children, so the child ids decode back to uuids.
Closes #10496.

Signed-off-by: Filip Sajdak <filip.sajdak@siili.com>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
This commit is contained in:
Filip Sajdak 2026-08-04 16:49:12 +02:00 committed by Andrey Antukh
parent 23ea2bbad6
commit 3fba272848
2 changed files with 105 additions and 1 deletions

View File

@ -10,6 +10,7 @@
[app.binfile.common :as bfc]
[app.binfile.v3 :as v3]
[app.common.features :as cfeat]
[app.common.files.validate :as cfv]
[app.common.pprint :as pp]
[app.common.thumbnails :as thc]
[app.common.types.shape :as cts]
@ -86,6 +87,102 @@
(dissoc file :data)))
(def ^:private svg-raw-page-id (uuid/custom 1 1))
(def ^:private svg-raw-root-id (uuid/custom 3 1))
(def ^:private svg-raw-child-id (uuid/custom 3 2))
(defn- prepare-svg-raw-file
"A file containing an svg-raw subtree (an svg-raw parent with an
svg-raw child), which is what importing an SVG produces."
[profile]
(let [page-id svg-raw-page-id
root-id svg-raw-root-id
child-id svg-raw-child-id
file (th/create-file* 1 {:profile-id (:id profile)
:project-id (:default-project-id profile)
:is-shared false})]
(update-file!
:file-id (:id file)
:profile-id (:id profile)
:revn 0
:vern 0
:changes
[{:type :add-page
:name "page 1"
:id page-id}])
(update-file!
:file-id (:id file)
:profile-id (:id profile)
:revn 0
:vern 0
:changes
[{:type :add-obj
:page-id page-id
:id root-id
:parent-id uuid/zero
:frame-id uuid/zero
:components-v2 true
:obj (cts/setup-shape
{:id root-id
:name "svg-root"
:frame-id uuid/zero
:parent-id uuid/zero
:type :svg-raw
:content {:tag :svg :attrs {} :content []}})}
{:type :add-obj
:page-id page-id
:id child-id
:parent-id root-id
:frame-id uuid/zero
:components-v2 true
:obj (cts/setup-shape
{:id child-id
:name "svg-text"
:frame-id uuid/zero
:parent-id root-id
:type :svg-raw
:content {:tag :text :attrs {} :content []}})}])
(dissoc file :data)))
(t/deftest import-binfile-v3-preserves-svg-raw-children
(let [profile (th/create-profile* 1)
file (prepare-svg-raw-file profile)
output (tmp/tempfile :suffix ".zip")]
(v3/export-files!
(-> th/*system*
(assoc ::bfc/ids #{(:id file)})
(assoc ::bfc/embed-assets false)
(assoc ::bfc/include-libraries false))
(io/output-stream output))
(let [result (-> th/*system*
(assoc ::bfc/project-id (:default-project-id profile))
(assoc ::bfc/profile-id (:id profile))
(assoc ::bfc/input output)
(v3/import-files!))
imported (:result (th/command! {::th/type :get-file
::rpc/profile-id (:id profile)
:id (first result)
:components-v2 true}))
root (get-in imported [:data :pages-index svg-raw-page-id
:objects svg-raw-root-id])]
(t/is (= (count result) 1))
;; The child ids of an svg-raw shape must survive the JSON round
;; trip as uuids; when they came back as plain strings they no
;; longer resolved against the objects map.
(t/is (every? uuid? (:shapes root)))
(t/is (= [svg-raw-child-id] (vec (:shapes root))))
;; ...so the imported file passes referential integrity instead
;; of failing with :child-not-found on the next update-file.
(t/is (nil? (cfv/validate-file imported []))))))
(t/deftest export-binfile-v3
(let [profile (th/create-profile* 1)
file (prepare-simple-file profile)

View File

@ -259,7 +259,14 @@
[:map {:title "CircleAttrs"}])
(def ^:private schema:svg-raw-attrs
[:map {:title "SvgRawAttrs"}])
[:map {:title "SvgRawAttrs"}
;; An svg-raw shape can be a container: importing an SVG builds a
;; tree of svg-raw shapes, and `cfh/group-like-shape?` treats an
;; svg-raw with children as group-like. Declaring `:shapes` here
;; keeps the child ids typed as uuid, so a JSON round trip (binfile
;; export/import) decodes them back to uuids instead of leaving
;; strings that no longer resolve against the objects map.
[:shapes {:optional true} [:vector {:gen/max 10} ::sm/uuid]]])
(def schema:image-attrs
[:map {:title "ImageAttrs"}