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 %}
-
Error reports (last 200)
+
Error reports (last 200)
+ [GO BACK]
+
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)))