diff --git a/backend/resources/app/templates/debug.tmpl b/backend/resources/app/templates/debug.tmpl index a630e8fbd2..65d3d7614c 100644 --- a/backend/resources/app/templates/debug.tmpl +++ b/backend/resources/app/templates/debug.tmpl @@ -17,38 +17,6 @@ Debug Main Page CLICK HERE TO SEE THE ERROR REPORTS -
- Download file data: - Given an FILE-ID, downloads the file data as file. The file data is encoded using transit. -
-
- -
-
- - -
-
-
- -
- Upload File Data: - Create a new file on your draft projects using the file downloaded from the previous section. -
-
- -
-
- - -
- -
- -
-
-
-
Profile Management
@@ -81,6 +49,50 @@ Debug Main Page +
+ +
+ Download RAW file data: + Given an FILE-ID, downloads the file AS-IS (no validation + checks, just exports the file data and related objects in raw) + +
+
+ WARNING: this operation does not performs any checks +
+ +
+ +
+
+ + +
+ +
+ +
+ Upload File Data: + Create a new file on your draft projects using the file downloaded from the previous section. +
+
+ WARNING: this operation does not performs any checks +
+
+
+ +
+
+ + +
+ +
+ +
+
+
+
Export binfile: @@ -88,7 +100,7 @@ Debug Main Page the related libraries in a single custom formatted binary file. -
+
@@ -116,7 +128,7 @@ Debug Main Page Import binfile: Import penpot file in binary format. - +
@@ -130,79 +142,27 @@ Debug Main Page
- Reset file version - Allows reset file data version to a specific number/ - - -
- -
-
- -
- -
- - -
- - This is a just a security double check for prevent non intentional submits. - -
- - -
- -
- -
-
- -
-

Feature Flags

-
- Enable + Feature Flags for Team Add a feature flag to a team -
+
- +
- - -
- - Do not check if the feature is supported - -
- -
- - -
- - This is a just a security double check for prevent non intentional submits. - -
- -
- -
-
-
-
- Disable - Remove a feature flag from a team -
-
- -
-
- +
diff --git a/backend/resources/app/templates/error-list.tmpl b/backend/resources/app/templates/error-list.tmpl index b8af22fb89..c328700ecf 100644 --- a/backend/resources/app/templates/error-list.tmpl +++ b/backend/resources/app/templates/error-list.tmpl @@ -7,7 +7,9 @@ penpot - error list {% block content %}
diff --git a/backend/src/app/binfile/common.clj b/backend/src/app/binfile/common.clj index b1a131ef10..072327792b 100644 --- a/backend/src/app/binfile/common.clj +++ b/backend/src/app/binfile/common.clj @@ -155,7 +155,7 @@ (defn decode-file "A general purpose file decoding function that resolves all external pointers, run migrations and return plain vanilla file map" - [cfg {:keys [id] :as file}] + [cfg {:keys [id] :as file} & {:keys [migrate?] :or {migrate? true}}] (binding [pmap/*load-fn* (partial feat.fdata/load-pointer cfg id)] (let [file (->> file (feat.fmigr/resolve-applied-migrations cfg) @@ -168,7 +168,7 @@ (update :data feat.fdata/process-pointers deref) (update :data feat.fdata/process-objects (partial into {})) (update :data assoc :id id) - (fmg/migrate-file libs))))) + (cond-> migrate? (fmg/migrate-file libs)))))) (defn get-file "Get file, resolve all features and apply migrations. diff --git a/backend/src/app/features/file_migrations.clj b/backend/src/app/features/file_migrations.clj index beec865555..9552d78ba4 100644 --- a/backend/src/app/features/file_migrations.clj +++ b/backend/src/app/features/file_migrations.clj @@ -37,3 +37,9 @@ {::db/return-keys false ::sql/on-conflict-do-nothing true}) (db/get-update-count)))) + +(defn reset-migrations! + "Replace file migrations" + [conn {:keys [id] :as file}] + (db/delete! conn :file-migration {:file-id id}) + (upsert-migrations! conn file)) diff --git a/backend/src/app/http/debug.clj b/backend/src/app/http/debug.clj index 5e05962566..f4d835b417 100644 --- a/backend/src/app/http/debug.clj +++ b/backend/src/app/http/debug.clj @@ -15,9 +15,11 @@ [app.common.features :as cfeat] [app.common.logging :as l] [app.common.pprint :as pp] + [app.common.transit :as t] [app.common.uuid :as uuid] [app.config :as cf] [app.db :as db] + [app.features.file-migrations :as feat.fmig] [app.http.session :as session] [app.rpc.commands.auth :as auth] [app.rpc.commands.files-create :refer [create-file]] @@ -50,26 +52,26 @@ {::yres/status 200 ::yres/headers {"content-type" "text/html"} ::yres/body (-> (io/resource "app/templates/debug.tmpl") - (tmpl/render {:version (:full cf/version)}))}) + (tmpl/render {:version (:full cf/version) + :supported-features cfeat/supported-features}))}) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; FILE CHANGES ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -(defn prepare-response - [body] - (let [headers {"content-type" "application/transit+json"}] - {::yres/status 200 - ::yres/body body - ::yres/headers headers})) +(defn- get-resolved-file + [cfg file-id] + (some-> (bfc/get-file cfg file-id :migrate? false) + (update :data blob/encode))) -(defn prepare-download-response - [body filename] - (let [headers {"content-disposition" (str "attachment; filename=" filename) - "content-type" "application/octet-stream"}] - {::yres/status 200 - ::yres/body body - ::yres/headers headers})) +(defn prepare-download + [file filename] + {::yres/status 200 + ::yres/headers + {"content-disposition" (str "attachment; filename=" filename ".json") + "content-type" "application/octet-stream"} + ::yres/body + (t/encode file {:type :json-verbose})}) (def sql:retrieve-range-of-changes "select revn, changes from file_change where file_id=? and revn >= ? and revn <= ? order by revn") @@ -77,45 +79,51 @@ (def sql:retrieve-single-change "select revn, changes, data from file_change where file_id=? and revn = ?") -(defn- retrieve-file-data - [{:keys [::db/pool]} {:keys [params ::session/profile-id] :as request}] +(defn- download-file-data + [cfg {:keys [params ::session/profile-id] :as request}] (let [file-id (some-> params :file-id parse-uuid) - revn (some-> params :revn parse-long) filename (str file-id)] (when-not file-id (ex/raise :type :validation :code :missing-arguments)) - (let [data (if (integer? revn) - (some-> (db/exec-one! pool [sql:retrieve-single-change file-id revn]) :data) - (some-> (db/get-by-id pool :file file-id) :data))] - - (when-not data - (ex/raise :type :not-found - :code :enpty-data - :hint "empty response")) + (if-let [file (get-resolved-file cfg file-id)] (cond (contains? params :download) - (prepare-download-response data filename) + (prepare-download file filename) (contains? params :clone) - (let [profile (profile/get-profile pool profile-id) - project-id (:default-project-id profile)] + (db/tx-run! cfg + (fn [{:keys [::db/conn] :as cfg}] + (let [profile (profile/get-profile conn profile-id) + project-id (:default-project-id profile) + file (-> (create-file cfg {:id (uuid/next) + :name (str "Cloned: " (:name file)) + :features (:features file) + :project-id project-id + :profile-id profile-id}) + (assoc :data (:data file)) + (assoc :migrations (:migrations file)))] - (db/run! pool (fn [{:keys [::db/conn] :as cfg}] - (create-file cfg {:id file-id - :name (str "Cloned file: " filename) - :project-id project-id - :profile-id profile-id}) - (db/update! conn :file - {:data data} - {:id file-id}) - {::yres/status 201 - ::yres/body "OK CREATED"}))) + (feat.fmig/reset-migrations! conn file) + (db/update! conn :file + {:data (:data file)} + {:id (:id file)} + {::db/return-keys false}) + + + {::yres/status 201 + ::yres/body "OK CLONED"}))) :else - (prepare-response (blob/decode data)))))) + (ex/raise :type :validation + :code :invalid-params + :hint "invalid button")) + + (ex/raise :type :not-found + :code :enpty-data + :hint "empty response")))) (defn- is-file-exists? [pool id] @@ -123,81 +131,61 @@ (-> (db/exec-one! pool [sql id]) :exists))) (defn- upload-file-data - [{:keys [::db/pool]} {:keys [::session/profile-id params] :as request}] + [{:keys [::db/pool] :as cfg} {:keys [::session/profile-id params] :as request}] (let [profile (profile/get-profile pool profile-id) project-id (:default-project-id profile) - data (some-> params :file :path io/read*)] + file (some-> params :file :path io/read* t/decode)] - (if (and data project-id) - (let [fname (str "Imported file *: " (dt/now)) + (if (and file project-id) + (let [fname (str "Imported: " (:name file) "(" (dt/now) ")") reuse-id? (contains? params :reuseid) file-id (or (and reuse-id? (ex/ignoring (-> params :file :filename parse-uuid))) (uuid/next))] (if (and reuse-id? file-id (is-file-exists? pool file-id)) - (do - (db/update! pool :file - {:data data - :deleted-at nil} - {:id file-id}) - {::yres/status 200 - ::yres/body "OK UPDATED"}) + (db/tx-run! cfg + (fn [{:keys [::db/conn] :as cfg}] + (db/update! conn :file + {:data (:data file) + :features (into-array (:features file)) + :deleted-at nil} + {:id file-id} + {::db/return-keys false}) + (feat.fmig/reset-migrations! conn file) + {::yres/status 200 + ::yres/body "OK UPDATED"})) + + (db/tx-run! cfg + (fn [{:keys [::db/conn] :as cfg}] + (let [file (-> (create-file cfg {:id file-id + :name fname + :features (:features file) + :project-id project-id + :profile-id profile-id}) + (assoc :data (:data file)) + (assoc :migrations (:migrations file)))] - (db/run! pool (fn [{:keys [::db/conn] :as cfg}] - (create-file cfg {:id file-id - :name fname - :project-id project-id - :profile-id profile-id}) (db/update! conn :file - {:data data} - {:id file-id}) + {:data (:data file)} + {:id file-id} + {::db/return-keys false}) + (feat.fmig/reset-migrations! conn file) {::yres/status 201 - ::yres/body "OK CREATED"})))) + ::yres/body "OK CREATED"}))))) - {::yres/status 500 - ::yres/body "ERROR"}))) + (ex/raise :type :validation + :code :invalid-params + :hint "invalid file uploaded")))) -(defn file-data-handler +(defn raw-export-import-handler [cfg request] (case (yreq/method request) - :get (retrieve-file-data cfg request) + :get (download-file-data cfg request) :post (upload-file-data cfg request) (ex/raise :type :http :code :method-not-found))) -(defn file-changes-handler - [{:keys [::db/pool]} {:keys [params] :as request}] - (letfn [(retrieve-changes [file-id revn] - (if (str/includes? revn ":") - (let [[start end] (->> (str/split revn #":") - (map str/trim) - (map parse-long))] - (some->> (db/exec! pool [sql:retrieve-range-of-changes file-id start end]) - (map :changes) - (map blob/decode) - (mapcat identity) - (vec))) - - (if-let [revn (parse-long revn)] - (let [item (db/exec-one! pool [sql:retrieve-single-change file-id revn])] - (some-> item :changes blob/decode vec)) - (ex/raise :type :validation :code :invalid-arguments))))] - - (let [file-id (some-> params :id parse-uuid) - revn (or (some-> params :revn parse-long) "latest") - filename (str file-id)] - - (when (or (not file-id) (not revn)) - (ex/raise :type :validation - :code :invalid-arguments - :hint "missing arguments")) - - (let [data (retrieve-changes file-id revn)] - (if (contains? params :download) - (prepare-download-response data filename) - (prepare-response data)))))) - ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; ERROR BROWSER ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -430,49 +418,49 @@ ::yres/body "OK"})) -(defn- add-team-feature - [{:keys [params] :as request}] - (let [team-id (some-> params :team-id d/parse-uuid) - feature (some-> params :feature str) +(defn- handle-team-features + [cfg {:keys [params] :as request}] + (let [team-id (some-> params :team-id d/parse-uuid) + feature (some-> params :feature str) + action (some-> params :action) skip-check (contains? params :skip-check)] - (when-not (contains? params :force) - (ex/raise :type :validation - :code :missing-force - :hint "missing force checkbox")) - (when (nil? team-id) (ex/raise :type :validation :code :invalid-team-id :hint "provided invalid team id")) - (srepl/enable-team-feature! team-id feature :skip-check skip-check) + (if (= action "show") + (let [team (db/run! cfg teams/get-team-info {:id team-id})] + {::yres/status 200 + ::yres/headers {"content-type" "text/plain"} + ::yres/body (apply str "Team features:\n" + (->> (:features team) + (map (fn [feature] + (str "- " feature "\n")))))}) - {::yres/status 200 - ::yres/headers {"content-type" "text/plain"} - ::yres/body "OK"})) + (do + (when-not (contains? params :force) + (ex/raise :type :validation + :code :missing-force + :hint "missing force checkbox")) -(defn- remove-team-feature - [{:keys [params] :as request}] - (let [team-id (some-> params :team-id d/parse-uuid) - feature (some-> params :feature str) - skip-check (contains? params :skip-check)] + (cond + (= action "enable") + (srepl/enable-team-feature! team-id feature :skip-check skip-check) - (when-not (contains? params :force) - (ex/raise :type :validation - :code :missing-force - :hint "missing force checkbox")) + (= action "disable") + (srepl/disable-team-feature! team-id feature :skip-check skip-check) - (when (nil? team-id) - (ex/raise :type :validation - :code :invalid-team-id - :hint "provided invalid team id")) + :else + (ex/raise :type :validation + :code :invalid-action + :hint (str "invalid action: " action))) - (srepl/disable-team-feature! team-id feature :skip-check skip-check) - {::yres/status 200 - ::yres/headers {"content-type" "text/plain"} - ::yres/body "OK"})) + {::yres/status 200 + ::yres/headers {"content-type" "text/plain"} + ::yres/body "OK"})))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; OTHER SMALL VIEWS/HANDLERS @@ -525,6 +513,25 @@ (ex/raise :type :authentication :code :only-admins-allowed)))))}) +(def errors + (letfn [(handle-error [cause] + (when-let [data (ex-data cause)] + (when (= :validation (:type data)) + (str "Error: " (or (:hint data) (ex-message cause)) "\n"))))] + {:name ::errors + :compile + (fn [& _params] + (fn [handler] + (fn [request] + (try + (handler request) + (catch Throwable cause + (let [body (or (handle-error cause) + (ex/format-throwable cause))] + {::yres/status 400 + ::yres/headers {"content-type" "text/plain"} + ::yres/body body}))))))})) + (defmethod ig/assert-key ::routes [_ params] (assert (db/pool? (::db/pool params)) "expected a valid database pool") @@ -540,15 +547,14 @@ ["/changelog" {:handler (partial changelog-handler cfg)}] ["/error/:id" {:handler (partial error-handler cfg)}] ["/error" {:handler (partial error-list-handler cfg)}] - ["/actions/resend-email-verification" - {:handler (partial resend-email-notification cfg)}] - ["/actions/reset-file-version" - {:handler (partial reset-file-version cfg)}] - ["/actions/add-team-feature" - {:handler (partial add-team-feature)}] - ["/actions/remove-team-feature" - {:handler (partial remove-team-feature)}] - ["/file/export" {:handler (partial export-handler cfg)}] - ["/file/import" {:handler (partial import-handler cfg)}] - ["/file/data" {:handler (partial file-data-handler cfg)}] - ["/file/changes" {:handler (partial file-changes-handler cfg)}]]]) + ["/actions" {:middleware [[errors]]} + ["/resend-email-verification" + {:handler (partial resend-email-notification cfg)}] + ["/reset-file-version" + {:handler (partial reset-file-version cfg)}] + ["/handle-team-features" + {:handler (partial handle-team-features cfg)}] + ["/file-export" {:handler (partial export-handler cfg)}] + ["/file-import" {:handler (partial import-handler cfg)}] + ["/file-raw-export-import" {:handler (partial raw-export-import-handler cfg)}]]]]) + diff --git a/backend/src/app/rpc/commands/files_create.clj b/backend/src/app/rpc/commands/files_create.clj index fc9dcb7fef..a4c9069e07 100644 --- a/backend/src/app/rpc/commands/files_create.clj +++ b/backend/src/app/rpc/commands/files_create.clj @@ -7,7 +7,6 @@ (ns app.rpc.commands.files-create (:require [app.binfile.common :as bfc] - [app.common.data.macros :as dm] [app.common.features :as cfeat] [app.common.schema :as sm] [app.common.types.file :as ctf] @@ -41,9 +40,7 @@ :or {is-shared false revn 0 create-page true} :as params}] - (dm/assert! - "expected a valid connection" - (db/connection? conn)) + (assert (db/connection? conn) "expected a valid connection") (binding [pmap/*tracked* (pmap/create-tracked) cfeat/*current* features] diff --git a/backend/src/app/rpc/commands/teams.clj b/backend/src/app/rpc/commands/teams.clj index bb23c6997c..a099223b83 100644 --- a/backend/src/app/rpc/commands/teams.clj +++ b/backend/src/app/rpc/commands/teams.clj @@ -78,9 +78,10 @@ (defn decode-row [{:keys [features subscription] :as row}] - (cond-> row - (some? features) (assoc :features (db/decode-pgarray features #{})) - (some? subscription) (assoc :subscription (db/decode-transit-pgobject subscription)))) + (when row + (cond-> row + (some? features) (assoc :features (db/decode-pgarray features #{})) + (some? subscription) (assoc :subscription (db/decode-transit-pgobject subscription))))) ;; FIXME: move @@ -461,11 +462,12 @@ ;; --- COMMAND QUERY: get-team-info -(defn- get-team-info +(defn get-team-info [{:keys [::db/conn] :as cfg} {:keys [id] :as params}] - (db/get* conn :team - {:id id} - {::sql/columns [:id :is-default]})) + (-> (db/get* conn :team + {:id id} + {::sql/columns [:id :is-default :features]}) + (decode-row))) (sv/defmethod ::get-team-info "Retrieve minimal team info by its ID." diff --git a/common/src/app/common/data.cljc b/common/src/app/common/data.cljc index bb25641333..d06da2ae4c 100644 --- a/common/src/app/common/data.cljc +++ b/common/src/app/common/data.cljc @@ -9,17 +9,16 @@ data resources." (:refer-clojure :exclude [read-string hash-map merge name update-vals parse-double group-by iteration concat mapcat - parse-uuid max min regexp?]) + parse-uuid max min regexp? array?]) #?(:cljs (:require-macros [app.common.data])) (:require - #?(:cljs [cljs.core :as c] - :clj [clojure.core :as c]) #?(:cljs [cljs.reader :as r] :clj [clojure.edn :as r]) #?(:cljs [goog.array :as garray]) [app.common.math :as mth] + [clojure.core :as c] [clojure.set :as set] [cuerdas.core :as str] [linked.map :as lkm] @@ -167,6 +166,15 @@ ;; Data Structures Access & Manipulation ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +(defn array? + [o] + #?(:cljs + (c/array? o) + :clj + (if (some? o) + (.isArray (class o)) + false))) + (defn not-empty? [coll] (boolean (seq coll)))