From 9474700d09a3b8c15792a96cda53854d2b49e13a Mon Sep 17 00:00:00 2001 From: Aitor Date: Tue, 5 Dec 2023 15:20:36 +0100 Subject: [PATCH 1/7] :bug: Fix color picker not rendering Latin1 svgs --- .../main/ui/workspace/viewport/pixel_overlay.cljs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/frontend/src/app/main/ui/workspace/viewport/pixel_overlay.cljs b/frontend/src/app/main/ui/workspace/viewport/pixel_overlay.cljs index ec83437d9..8c133aeec 100644 --- a/frontend/src/app/main/ui/workspace/viewport/pixel_overlay.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/pixel_overlay.cljs @@ -7,6 +7,7 @@ (ns app.main.ui.workspace.viewport.pixel-overlay (:require [app.common.data :as d] + [app.common.data.macros :as dm] [app.common.pages.helpers :as cph] [app.common.uuid :as uuid] [app.main.data.modal :as modal] @@ -33,7 +34,7 @@ (let [image-nodes (dom/query-all svg-node "image:not([href^=data])") noop-fn (constantly nil)] (if (empty? image-nodes) - (rx/of nil) + (rx/of svg-node) (->> (rx/from image-nodes) (rx/mapcat (fn [image] @@ -43,7 +44,8 @@ (rx/mapcat wapi/read-file-as-data-url) (rx/tap (fn [data] (dom/set-attribute! image "href" data))) - (rx/reduce noop-fn))))))))) + (rx/reduce noop-fn))))) + (rx/map (fn [_] svg-node)))))) (defn- svg-as-data-url "Transforms SVG as data-url resolving any blob, http or https url to @@ -51,7 +53,11 @@ [svg] (let [svg-clone (.cloneNode svg true)] (->> (resolve-svg-images! svg-clone) - (rx/map (fn [_] (dom/svg-node->data-uri svg-clone)))))) + (rx/mapcat (fn [svg-node] + (let [xml (js/XMLSerializer.) + xmlstr (.serializeToString xml svg-node)] + (->> (rx/of xmlstr) + (rx/map #(dm/str "data:image/svg+xml;charset=utf-8," (js/encodeURIComponent %)))))))))) (defn format-viewbox [vbox] (str/join " " [(:x vbox 0) From 2fa06baa36bb87b5100f0615f753197de8af258a Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Wed, 24 Jan 2024 17:10:56 +0100 Subject: [PATCH 2/7] :bug: Fix incorrect props handling on profile registration --- backend/src/app/rpc/commands/auth.clj | 4 +- .../test/backend_tests/rpc_profile_test.clj | 50 +++++++------------ 2 files changed, 20 insertions(+), 34 deletions(-) diff --git a/backend/src/app/rpc/commands/auth.clj b/backend/src/app/rpc/commands/auth.clj index d765a5598..fd0e20600 100644 --- a/backend/src/app/rpc/commands/auth.clj +++ b/backend/src/app/rpc/commands/auth.clj @@ -325,7 +325,9 @@ (defn register-profile [{:keys [::db/conn] :as cfg} {:keys [token fullname] :as params}] (let [claims (tokens/verify (::main/props cfg) {:token token :iss :prepared-register}) - params (assoc claims :fullname fullname) + params (-> claims + (into params) + (assoc :fullname fullname)) is-active (or (:is-active params) (not (contains? cf/flags :email-verification))) diff --git a/backend/test/backend_tests/rpc_profile_test.clj b/backend/test/backend_tests/rpc_profile_test.clj index a28f186c8..d7180461b 100644 --- a/backend/test/backend_tests/rpc_profile_test.clj +++ b/backend/test/backend_tests/rpc_profile_test.clj @@ -11,6 +11,7 @@ [app.db :as db] [app.rpc :as-alias rpc] [app.auth :as auth] + [app.rpc.commands.profile :as profile] [app.tokens :as tokens] [app.util.time :as dt] [backend-tests.helpers :as th] @@ -240,41 +241,12 @@ token (get-in out [:result :token])] (t/is (string? token)) - - ;; try register without token - (let [data {::th/type :register-profile - :fullname "foobar" - :accept-terms-and-privacy true} - out (th/command! data)] - (let [error (:error out)] - (t/is (th/ex-info? error)) - (t/is (th/ex-of-type? error :validation)) - (t/is (th/ex-of-code? error :spec-validation)))) - - ;; try correct register - (let [data {::th/type :register-profile - :token token - :fullname "foobar" - :accept-terms-and-privacy true - :accept-newsletter-subscription true}] - (let [{:keys [result error]} (th/command! data)] - (t/is (nil? error)))) - )) - -(t/deftest prepare-register-and-register-profile-1 - (let [data {::th/type :prepare-register-profile - :email "user@example.com" - :password "foobar"} - out (th/command! data) - token (get-in out [:result :token])] - (t/is (string? token)) - - ;; try register without token (let [data {::th/type :register-profile :fullname "foobar" :accept-terms-and-privacy true} out (th/command! data)] + ;; (th/print-result! out) (let [error (:error out)] (t/is (th/ex-info? error)) (t/is (th/ex-of-type? error :validation)) @@ -284,12 +256,24 @@ (let [data {::th/type :register-profile :token token :fullname "foobar" + :utm_campaign "utma" + :mtm_campaign "mtma" :accept-terms-and-privacy true :accept-newsletter-subscription true}] - (let [{:keys [result error] :as out} (th/command! data)] - ;; (th/print-result! out) + (let [{:keys [result error]} (th/command! data)] (t/is (nil? error)))) - )) + + (let [profile (some-> (th/db-get :profile {:email "user@example.com"}) + (profile/decode-row))] + (t/is (= "penpot" (:auth-backend profile))) + (t/is (= "foobar" (:fullname profile))) + (t/is (false? (:is-active profile))) + (t/is (uuid? (:default-team-id profile))) + (t/is (uuid? (:default-project-id profile))) + + (let [props (:props profile)] + (t/is (= "utma" (:penpot/utm-campaign props))) + (t/is (= "mtma" (:penpot/mtm-campaign props))))))) (t/deftest prepare-register-and-register-profile-2 (with-redefs [app.rpc.commands.auth/register-retry-threshold (dt/duration 500)] From d039df6b7378662a6df1ff89c9e8bb767c76d30b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Moya?= Date: Tue, 18 Jun 2024 13:07:34 +0200 Subject: [PATCH 3/7] :white_check_mark: Add tests for detach with swapped copies --- .../app/common/test_helpers/components.cljc | 32 +-- .../app/common/test_helpers/compositions.cljc | 22 ++ common/test/cases/detach-with-swap.penpot | Bin 0 -> 20015 bytes common/test/cases/remove-swap-slots.penpot | Bin 0 -> 12794 bytes .../logic/comp_detach_with_swap_test.cljc | 197 ++++++++++++++++++ .../logic/comp_remove_swap_slots_test.cljc | 1 - 6 files changed, 238 insertions(+), 14 deletions(-) create mode 100644 common/test/cases/detach-with-swap.penpot create mode 100644 common/test/cases/remove-swap-slots.penpot create mode 100644 common/test/common_tests/logic/comp_detach_with_swap_test.cljc diff --git a/common/src/app/common/test_helpers/components.cljc b/common/src/app/common/test_helpers/components.cljc index dadd2feac..150bbeeb4 100644 --- a/common/src/app/common/test_helpers/components.cljc +++ b/common/src/app/common/test_helpers/components.cljc @@ -6,6 +6,7 @@ (ns app.common.test-helpers.components (:require + [app.common.data :as d] [app.common.data.macros :as dm] [app.common.files.changes-builder :as pcb] [app.common.files.helpers :as cfh] @@ -64,13 +65,12 @@ [file id] (ctkl/get-component (:data file) id)) -(defn set-child-label - [file shape-label child-idx label] - (let [id (-> (ths/get-shape file shape-label) - :shapes - (nth child-idx))] - (when id - (thi/set-id! label id)))) +(defn- set-children-labels! + [file shape-label children-labels] + (doseq [[label id] + (d/zip children-labels (cfh/get-children-ids (-> (thf/current-page file) :objects) + (thi/id shape-label)))] + (thi/set-id! label id))) (defn instantiate-component [file component-label copy-root-label & {:keys [parent-label library children-labels] :as params}] @@ -103,6 +103,7 @@ (and (some? parent) (ctn/in-any-component? (:objects page) parent)) (dissoc :component-root)) + file' (ctf/update-file-data file (fn [file-data] @@ -128,14 +129,14 @@ true))) $ (remove #(= (:id %) (:id copy-root')) copy-shapes)))))] + (when children-labels - (dotimes [idx (count children-labels)] - (set-child-label file' copy-root-label idx (nth children-labels idx)))) + (set-children-labels! file' copy-root-label children-labels)) file')) (defn component-swap - [file shape-label new-component-label new-shape-label & {:keys [library] :as params}] + [file shape-label new-component-label new-shape-label & {:keys [library children-labels] :as params}] (let [shape (ths/get-shape file shape-label) library (or library file) libraries {(:id library) library} @@ -147,10 +148,15 @@ ;; Store the properties that need to be maintained when the component is swapped keep-props-values (select-keys shape ctk/swap-keep-attrs) - [new_shape _ changes] (-> (pcb/empty-changes nil (:id page)) - (cll/generate-component-swap objects shape (:data file) page libraries id-new-component 0 nil keep-props-values))] + (cll/generate-component-swap objects shape (:data file) page libraries id-new-component 0 nil keep-props-values)) + + file' (thf/apply-changes file changes)] (thi/set-id! new-shape-label (:id new_shape)) - (thf/apply-changes file changes))) + + (when children-labels + (set-children-labels! file' new-shape-label children-labels)) + + file')) diff --git a/common/src/app/common/test_helpers/compositions.cljc b/common/src/app/common/test_helpers/compositions.cljc index 6d89fa475..82ebf5c58 100644 --- a/common/src/app/common/test_helpers/compositions.cljc +++ b/common/src/app/common/test_helpers/compositions.cljc @@ -58,6 +58,28 @@ :parent-label frame-label} child-params)))) +(defn add-minimal-component + [file component-label root-label + & {:keys [component-params root-params]}] + ;; Generated shape tree: + ;; {:root-label} [:name Frame1] # [Component :component-label] + (-> file + (add-frame root-label root-params) + (thc/make-component component-label root-label component-params))) + +(defn add-minimal-component-with-copy + [file component-label main-root-label copy-root-label + & {:keys [component-params main-root-params copy-root-params]}] + ;; Generated shape tree: + ;; {:main-root-label} [:name Frame1] # [Component :component-label] + ;; :copy-root-label [:name Frame1] #--> [Component :component-label] :main-root-label + (-> file + (add-minimal-component component-label + main-root-label + :component-params component-params + :root-params main-root-params) + (thc/instantiate-component component-label copy-root-label copy-root-params))) + (defn add-simple-component [file component-label root-label child-label & {:keys [component-params root-params child-params]}] diff --git a/common/test/cases/detach-with-swap.penpot b/common/test/cases/detach-with-swap.penpot new file mode 100644 index 0000000000000000000000000000000000000000..2ff274b6d5d66acb190d02c7d0acb811d310874f GIT binary patch literal 20015 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 V^qDQ<+;Tl>8I)HskpOG~006U*nx+5% literal 0 HcmV?d00001 diff --git a/common/test/cases/remove-swap-slots.penpot b/common/test/cases/remove-swap-slots.penpot new file mode 100644 index 0000000000000000000000000000000000000000..0de71803b11f28a7330dd6fee443440a4211c57f GIT binary patch literal 12794 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

D@ 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 :r-ellipse + ;; :nested-ellipse [:name Ellipse, :type :circle] ---> :ellipse + ;; {:board-with-rectangle} [:name Board with rectangle, :type :frame] # [Component :c-board-with-rectangle] + ;; :nested-h-rectangle [:name Rectangle, :type :frame] @--> :r-rectangle + ;; :nested-rectangle [:name rectangle, :type :rect] ---> :rectangle + ;; {:big-board} [:name Big Board, :type :frame] # [Component :c-big-board] + ;; :h-board-with-ellipse [:name Board with ellipse, :type :frame] @--> :board-with-ellipse + ;; :nested2-h-ellipse [:name Ellipse, :type :frame] @--> :nested-h-ellipse + ;; :nested2-ellipse [:name Ellipse, :type :circle] ---> :nested-ellipse + (-> (thf/sample-file :file1) + + (tho/add-simple-component :c-ellipse :r-ellipse :ellipse + :root-params {:name "Ellipse"} + :child-params {:name "Ellipse" :type :circle}) + + (tho/add-simple-component :c-rectangle :r-rectangle :rectangle + :root-params {:name "Rectangle"} + :child-params {:name "rectangle" :type :rect}) + + (tho/add-frame :board-with-ellipse :name "Board with ellipse") + (thc/instantiate-component :c-ellipse :nested-h-ellipse :parent-label :board-with-ellipse + :children-labels [:nested-ellipse]) + (thc/make-component :c-board-with-ellipse :board-with-ellipse) + + (tho/add-frame :board-with-rectangle :name "Board with rectangle") + (thc/instantiate-component :c-rectangle :nested-h-rectangle :parent-label :board-with-rectangle + :children-labels [:nested-rectangle]) + (thc/make-component :c-board-with-rectangle :board-with-rectangle) + + (tho/add-frame :big-board :name "Big Board") + (thc/instantiate-component :c-board-with-ellipse + :h-board-with-ellipse + :parent-label :big-board + :children-labels [:nested2-h-ellipse :nested2-ellipse]) + (thc/make-component :c-big-board :big-board))) + +(t/deftest test-advance-when-not-swapped + (let [;; ==== Setup + file (-> (setup-file) + (thc/instantiate-component :c-big-board + :copy-big-board + :children-labels [:copy-h-board-with-ellipse + :copy-nested-h-ellipse + :copy-nested-ellipse])) + + page (thf/current-page file) + + ;; ==== Action + changes (cll/generate-detach-instance (-> (pcb/empty-changes nil) + (pcb/with-page page) + (pcb/with-objects (:objects page))) + page + {(:id file) file} + (thi/id :copy-big-board)) + file' (thf/apply-changes file changes) + + ;; ==== Get + copy-h-board-with-ellipse (ths/get-shape file' :copy-h-board-with-ellipse) + copy-nested-h-ellipse (ths/get-shape file' :copy-nested-h-ellipse) + copy-nested-ellipse (ths/get-shape file' :copy-nested-ellipse)] + + ;; ==== Check + + ;; In the normal case, children's ref (that pointed to the near main inside big-board) + ;; are advanced to point to the new near main inside board-with-ellipse. + (t/is (ctk/instance-root? copy-h-board-with-ellipse)) + (t/is (= (:shape-ref copy-h-board-with-ellipse) (thi/id :board-with-ellipse))) + (t/is (nil? (ctk/get-swap-slot copy-h-board-with-ellipse))) + + (t/is (ctk/instance-head? copy-nested-h-ellipse)) + (t/is (= (:shape-ref copy-nested-h-ellipse) (thi/id :nested-h-ellipse))) + (t/is (nil? (ctk/get-swap-slot copy-nested-h-ellipse))) + + (t/is (not (ctk/instance-head? copy-nested-ellipse))) + (t/is (= (:shape-ref copy-nested-ellipse) (thi/id :nested-ellipse))) + (t/is (nil? (ctk/get-swap-slot copy-nested-ellipse))))) + +(t/deftest test-dont-advance-when-swapped-copy + (let [;; ==== Setup + file (-> (setup-file) + (thc/instantiate-component :c-big-board + :copy-big-board + :children-labels [:copy-h-board-with-ellipse + :copy-nested-h-ellipse + :copy-nested-ellipse]) + (thc/component-swap :copy-h-board-with-ellipse + :c-board-with-rectangle + :copy-h-board-with-rectangle + :children-labels [:copy-nested-h-rectangle + :copy-nested-rectangle])) + + page (thf/current-page file) + + ;; ==== Action + changes (cll/generate-detach-instance (-> (pcb/empty-changes nil) + (pcb/with-page page) + (pcb/with-objects (:objects page))) + page + {(:id file) file} + (thi/id :copy-big-board)) + file' (thf/apply-changes file changes) + + ;; ==== Get + copy-h-board-with-rectangle (ths/get-shape file' :copy-h-board-with-rectangle) + copy-nested-h-rectangle (ths/get-shape file' :copy-nested-h-rectangle) + copy-nested-rectangle (ths/get-shape file' :copy-nested-rectangle)] + + ;; ==== Check + + ;; If the nested copy was swapped, there is no need to advance shape-refs, + ;; as they already pointing to the near main inside board-with-rectangle. + (t/is (ctk/instance-root? copy-h-board-with-rectangle)) + (t/is (= (:shape-ref copy-h-board-with-rectangle) (thi/id :board-with-rectangle))) + (t/is (nil? (ctk/get-swap-slot copy-h-board-with-rectangle))) + + (t/is (ctk/instance-head? copy-nested-h-rectangle)) + (t/is (= (:shape-ref copy-nested-h-rectangle) (thi/id :nested-h-rectangle))) + (t/is (nil? (ctk/get-swap-slot copy-nested-h-rectangle))) + + (t/is (not (ctk/instance-head? copy-nested-rectangle))) + (t/is (= (:shape-ref copy-nested-rectangle) (thi/id :nested-rectangle))) + (t/is (nil? (ctk/get-swap-slot copy-nested-rectangle))))) + +(t/deftest test-propagate-slot-when-swapped-main + (let [;; ==== Setup + file (-> (setup-file) + (thc/component-swap :nested2-h-ellipse + :c-rectangle + :nested2-h-rectangle + :children-labels [:nested2-rectangle]) + (thc/instantiate-component :c-big-board + :copy-big-board + :children-labels [:copy-h-board-with-ellipse + :copy-nested-h-rectangle + :copy-nested-rectangle])) + + page (thf/current-page file) + + ;; ==== Action + changes (cll/generate-detach-instance (-> (pcb/empty-changes nil) + (pcb/with-page page) + (pcb/with-objects (:objects page))) + page + {(:id file) file} + (thi/id :copy-big-board)) + file' (thf/apply-changes file changes) + + ;; ==== Get + copy-h-board-with-ellipse (ths/get-shape file' :copy-h-board-with-ellipse) + copy-nested-h-rectangle (ths/get-shape file' :copy-nested-h-rectangle) + copy-nested-rectangle (ths/get-shape file' :copy-nested-rectangle)] + + ;; ==== Check + + ;; This one is advanced normally, as it has not been swapped. + (t/is (ctk/instance-root? copy-h-board-with-ellipse)) + (t/is (= (:shape-ref copy-h-board-with-ellipse) (thi/id :board-with-ellipse))) + (t/is (nil? (ctk/get-swap-slot copy-h-board-with-ellipse))) + + ;; If the nested copy has been swapped in the main, it does advance, + ;; but the swap slot of the near main is propagated to the copy. + (t/is (ctk/instance-head? copy-nested-h-rectangle)) + (t/is (= (:shape-ref copy-nested-h-rectangle) (thi/id :r-rectangle))) + (t/is (= (ctk/get-swap-slot copy-nested-h-rectangle) (thi/id :nested-h-ellipse))) + + (t/is (not (ctk/instance-head? copy-nested-rectangle))) + (t/is (= (:shape-ref copy-nested-rectangle) (thi/id :rectangle))) + (t/is (nil? (ctk/get-swap-slot copy-nested-rectangle))))) + diff --git a/common/test/common_tests/logic/comp_remove_swap_slots_test.cljc b/common/test/common_tests/logic/comp_remove_swap_slots_test.cljc index e40dd2f1a..3bf5d8ceb 100644 --- a/common/test/common_tests/logic/comp_remove_swap_slots_test.cljc +++ b/common/test/common_tests/logic/comp_remove_swap_slots_test.cljc @@ -758,7 +758,6 @@ (t/is (some? blue-copy1')) (t/is (nil? (ctk/get-swap-slot blue-copy1'))))) - (t/deftest test-remove-swap-slot-detach (let [;; ==== Setup file (setup-file) From 00b4013385a23c388ce4d837e8753cfbf4954d45 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Tue, 18 Jun 2024 20:57:45 +0200 Subject: [PATCH 4/7] :sparkles: Forward external session id to backend --- backend/src/app/loggers/audit.clj | 7 +++++-- frontend/src/app/config.cljs | 13 ++++++++++--- frontend/src/app/main/data/events.cljs | 6 ++++-- frontend/src/app/main/repo.cljs | 3 ++- 4 files changed, 21 insertions(+), 8 deletions(-) diff --git a/backend/src/app/loggers/audit.clj b/backend/src/app/loggers/audit.clj index d89809f37..10797f41c 100644 --- a/backend/src/app/loggers/audit.clj +++ b/backend/src/app/loggers/audit.clj @@ -141,6 +141,7 @@ (::rpc/profile-id params) uuid/zero) + session-id (rreq/get-header request "x-external-session-id") props (-> (or (::replace-props resultm) (-> params (merge (::props resultm)) @@ -150,8 +151,10 @@ (clean-props)) token-id (::actoken/id request) - context (d/without-nils - {:access-token-id (some-> token-id str)})] + context (-> (::context resultm) + (assoc :external-session-id session-id) + (assoc :access-token-id (some-> token-id str)) + (d/without-nils))] {::type (or (::type resultm) (::rpc/type cfg)) diff --git a/frontend/src/app/config.cljs b/frontend/src/app/config.cljs index 4cfa49985..54ad1b37a 100644 --- a/frontend/src/app/config.cljs +++ b/frontend/src/app/config.cljs @@ -130,9 +130,16 @@ (def worker-uri (obj/get global "penpotWorkerURI" "/js/worker.js")) -(defn external-feature-flag [flag value] - (when-let [fn (obj/get global "externalFeatureFlag")] - (fn flag value))) +(defn external-feature-flag + [flag value] + (let [f (obj/get global "externalFeatureFlag")] + (when (fn? f) + (f flag value)))) + +(defn external-session-id + [] + (let [f (obj/get global "externalSessionId")] + (when (fn? f) (f)))) ;; --- Helper Functions diff --git a/frontend/src/app/main/data/events.cljs b/frontend/src/app/main/data/events.cljs index ec217339c..1e0cc623f 100644 --- a/frontend/src/app/main/data/events.cljs +++ b/frontend/src/app/main/data/events.cljs @@ -168,7 +168,7 @@ ptk/EffectEvent (effect [_ _ stream] (let [session (atom nil) - stopper (rx/filter (ptk/type? ::initialize) stream) + stopper (rx/filter (ptk/type? ::initialize) stream) buffer (atom #queue []) profile (->> (rx/from-atom storage {:emit-current-value? true}) (rx/map :profile) @@ -213,7 +213,9 @@ (let [session* (or @session (dt/now)) context (-> @context (merge (:context event)) - (assoc :session session*))] + (assoc :session session*) + (assoc :external-session-id (cf/external-session-id)) + (d/without-nils))] (reset! session session*) (-> event (assoc :timestamp (dt/now)) diff --git a/frontend/src/app/main/repo.cljs b/frontend/src/app/main/repo.cljs index ed71b827a..b6ff8dc1e 100644 --- a/frontend/src/app/main/repo.cljs +++ b/frontend/src/app/main/repo.cljs @@ -97,7 +97,8 @@ request {:method method :uri (u/join cf/public-uri "api/rpc/command/" nid) :credentials "include" - :headers {"accept" "application/transit+json,text/event-stream,*/*"} + :headers {"accept" "application/transit+json,text/event-stream,*/*" + "x-external-session-id" (cf/external-session-id)} :body (when (= method :post) (if form-data? (http/form-data params) From 504f833a539a3cb584c443731486bb7fbb4dde0f Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Tue, 18 Jun 2024 14:42:58 +0200 Subject: [PATCH 5/7] :bug: Fix global error handler incorrect body encoding --- backend/src/app/http.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/app/http.clj b/backend/src/app/http.clj index a696d5477..cacf15805 100644 --- a/backend/src/app/http.clj +++ b/backend/src/app/http.clj @@ -114,7 +114,7 @@ (partial not-found-handler request))) (on-error [cause request] - (let [{:keys [body] :as response} (errors/handle cause request)] + (let [{:keys [::rres/body] :as response} (errors/handle cause request)] (cond-> response (map? body) (-> (update ::rres/headers assoc "content-type" "application/transit+json") From 06bab212b53ce82e39d50e0d5c82e8f9135f96b7 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Tue, 18 Jun 2024 14:43:15 +0200 Subject: [PATCH 6/7] :bug: Set correct order for http middlewares --- backend/src/app/http.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/app/http.clj b/backend/src/app/http.clj index cacf15805..c45c95c1c 100644 --- a/backend/src/app/http.clj +++ b/backend/src/app/http.clj @@ -150,10 +150,10 @@ [["" {:middleware [[mw/server-timing] [mw/params] [mw/format-response] + [mw/errors errors/handle] [mw/parse-request] [session/soft-auth cfg] [actoken/soft-auth cfg] - [mw/errors errors/handle] [mw/restrict-methods]]} (::mtx/routes cfg) From 3363793d647093a72b9f683681e4e3cb5c50a09f Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Tue, 18 Jun 2024 14:44:25 +0200 Subject: [PATCH 7/7] :bug: Fix json encoding truncation issue --- backend/src/app/http/middleware.clj | 37 +++++++++++++++++----------- backend/src/app/util/objects_map.clj | 7 +++++- backend/src/app/util/pointer_map.clj | 11 ++++++++- 3 files changed, 38 insertions(+), 17 deletions(-) diff --git a/backend/src/app/http/middleware.clj b/backend/src/app/http/middleware.clj index 4ea815f07..a6eabd9a4 100644 --- a/backend/src/app/http/middleware.clj +++ b/backend/src/app/http/middleware.clj @@ -10,16 +10,13 @@ [app.common.logging :as l] [app.common.transit :as t] [app.config :as cf] - [app.util.json :as json] + [clojure.data.json :as json] [cuerdas.core :as str] [ring.request :as rreq] [ring.response :as rres] [yetti.adapter :as yt] [yetti.middleware :as ymw]) (:import - com.fasterxml.jackson.core.JsonParseException - com.fasterxml.jackson.core.io.JsonEOFException - com.fasterxml.jackson.databind.exc.MismatchedInputException io.undertow.server.RequestTooBigException java.io.InputStream java.io.OutputStream)) @@ -34,11 +31,22 @@ {:name ::params :compile (constantly ymw/wrap-params)}) -(def ^:private json-mapper - (json/mapper - {:encode-key-fn str/camel - :decode-key-fn (comp keyword str/kebab) - :pretty true})) +(defn- get-reader + ^java.io.BufferedReader + [request] + (let [^InputStream body (rreq/body request)] + (java.io.BufferedReader. + (java.io.InputStreamReader. body)))) + +(defn- read-json-key + [k] + (-> k str/kebab keyword)) + +(defn- write-json-key + [k] + (if (or (keyword? k) (symbol? k)) + (str/camel k) + (str k))) (defn wrap-parse-request [handler] @@ -53,8 +61,8 @@ (update :params merge params)))) (str/starts-with? header "application/json") - (with-open [^InputStream is (rreq/body request)] - (let [params (json/decode is json-mapper)] + (with-open [reader (get-reader request)] + (let [params (json/read reader :key-fn read-json-key)] (-> request (assoc :body-params params) (update :params merge params)))) @@ -74,9 +82,7 @@ :code :request-body-too-large :hint (ex-message cause)) - (or (instance? JsonEOFException cause) - (instance? JsonParseException cause) - (instance? MismatchedInputException cause)) + (instance? java.io.EOFException cause) (ex/raise :type :validation :code :malformed-json :hint (ex-message cause) @@ -128,7 +134,8 @@ (-write-body-to-stream [_ _ output-stream] (try (with-open [^OutputStream bos (buffered-output-stream output-stream buffer-size)] - (json/write! bos data json-mapper)) + (with-open [^java.io.OutputStreamWriter writer (java.io.OutputStreamWriter. bos)] + (json/write data writer :key-fn write-json-key))) (catch java.io.IOException _) (catch Throwable cause diff --git a/backend/src/app/util/objects_map.clj b/backend/src/app/util/objects_map.clj index 19a7bdea6..c7e4f42eb 100644 --- a/backend/src/app/util/objects_map.clj +++ b/backend/src/app/util/objects_map.clj @@ -19,7 +19,8 @@ [app.common.fressian :as fres] [app.common.transit :as t] [app.common.uuid :as uuid] - [clojure.core :as c]) + [clojure.core :as c] + [clojure.data.json :as json]) (:import clojure.lang.Counted clojure.lang.IHashEq @@ -83,6 +84,10 @@ ^:unsynchronized-mutable loaded? ^:unsynchronized-mutable modified?] + json/JSONWriter + (-write [this writter options] + (json/-write (into {} this) writter options)) + IHashEq (hasheq [this] (when-not hash diff --git a/backend/src/app/util/pointer_map.clj b/backend/src/app/util/pointer_map.clj index 16ce73bb0..ba84d3d4b 100644 --- a/backend/src/app/util/pointer_map.clj +++ b/backend/src/app/util/pointer_map.clj @@ -40,7 +40,8 @@ [app.common.transit :as t] [app.common.uuid :as uuid] [app.util.time :as dt] - [clojure.core :as c]) + [clojure.core :as c] + [clojure.data.json :as json]) (:import clojure.lang.Counted clojure.lang.IDeref @@ -75,6 +76,14 @@ ^:unsynchronized-mutable modified? ^:unsynchronized-mutable loaded?] + json/JSONWriter + (-write [this writter options] + (json/-write {:type "pointer" + :id (get-id this) + :meta (meta this)} + writter + options)) + IPointerMap (load! [_] (when-not *load-fn*