From 648c8e2152dc293c998b33aa554f746c1372ce38 Mon Sep 17 00:00:00 2001 From: Filip Sajdak Date: Tue, 4 Aug 2026 16:49:12 +0200 Subject: [PATCH] :bug: 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 Co-authored-by: Andrey Antukh --- backend/test/backend_tests/binfile_test.clj | 97 +++++++++++++++++++++ common/src/app/common/types/shape.cljc | 9 +- 2 files changed, 105 insertions(+), 1 deletion(-) diff --git a/backend/test/backend_tests/binfile_test.clj b/backend/test/backend_tests/binfile_test.clj index 232a44f87c..84310241d4 100644 --- a/backend/test/backend_tests/binfile_test.clj +++ b/backend/test/backend_tests/binfile_test.clj @@ -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) diff --git a/common/src/app/common/types/shape.cljc b/common/src/app/common/types/shape.cljc index d645e72ae1..46f608f390 100644 --- a/common/src/app/common/types/shape.cljc +++ b/common/src/app/common/types/shape.cljc @@ -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"}