Add several improvements to admin pannel

This commit is contained in:
Andrey Antukh 2025-07-08 13:17:03 +02:00
parent ea0044f69a
commit fa72bb4adf
8 changed files with 233 additions and 252 deletions

View file

@ -17,38 +17,6 @@ Debug Main Page
<desc><a href="/dbg/error">CLICK HERE TO SEE THE ERROR REPORTS</a> </desc> <desc><a href="/dbg/error">CLICK HERE TO SEE THE ERROR REPORTS</a> </desc>
</fieldset> </fieldset>
<fieldset>
<legend>Download file data:</legend>
<desc>Given an FILE-ID, downloads the file data as file. The file data is encoded using transit.</desc>
<form method="get" action="/dbg/file/data">
<div class="row">
<input type="text" style="width:300px" name="file-id" placeholder="file-id" />
</div>
<div class="row">
<input type="submit" name="download" value="Download" />
<input type="submit" name="clone" value="Clone" />
</div>
</form>
</fieldset>
<fieldset>
<legend>Upload File Data:</legend>
<desc>Create a new file on your draft projects using the file downloaded from the previous section.</desc>
<form method="post" enctype="multipart/form-data" action="/dbg/file/data">
<div class="row">
<input type="file" name="file" value="" />
</div>
<div class="row">
<label>Import with same id?</label>
<input type="checkbox" name="reuseid" />
</div>
<div class="row">
<input type="submit" value="Upload" />
</div>
</form>
</fieldset>
<fieldset> <fieldset>
<legend>Profile Management</legend> <legend>Profile Management</legend>
<form method="post" action="/dbg/actions/resend-email-verification"> <form method="post" action="/dbg/actions/resend-email-verification">
@ -81,6 +49,50 @@ Debug Main Page
</section> </section>
<section class="widget">
<fieldset>
<legend>Download RAW file data:</legend>
<desc>Given an FILE-ID, downloads the file AS-IS (no validation
checks, just exports the file data and related objects in raw)
<br/>
<br/>
<b>WARNING: this operation does not performs any checks</b>
</desc>
<form method="get" action="/dbg/actions/file-raw-export-import">
<div class="row">
<input type="text" style="width:300px" name="file-id" placeholder="file-id" />
</div>
<div class="row">
<input type="submit" name="download" value="Download" />
<input type="submit" name="clone" value="Clone" />
</div>
</form>
</fieldset>
<fieldset>
<legend>Upload File Data:</legend>
<desc>Create a new file on your draft projects using the file downloaded from the previous section.
<br/>
<br/>
<b>WARNING: this operation does not performs any checks</b>
</desc>
<form method="post" enctype="multipart/form-data" action="/dbg/actions/file-raw-export-import">
<div class="row">
<input type="file" name="file" value="" />
</div>
<div class="row">
<label>Import with same id?</label>
<input type="checkbox" name="reuseid" />
</div>
<div class="row">
<input type="submit" value="Upload" />
</div>
</form>
</fieldset>
</section>
<section class="widget"> <section class="widget">
<fieldset> <fieldset>
<legend>Export binfile:</legend> <legend>Export binfile:</legend>
@ -88,7 +100,7 @@ Debug Main Page
the related libraries in a single custom formatted binary the related libraries in a single custom formatted binary
file.</desc> file.</desc>
<form method="get" action="/dbg/file/export"> <form method="get" action="/dbg/actions/file-export">
<div class="row set-of-inputs"> <div class="row set-of-inputs">
<input type="text" style="width:300px" name="file-ids" placeholder="file-id" /> <input type="text" style="width:300px" name="file-ids" placeholder="file-id" />
<input type="text" style="width:300px" name="file-ids" placeholder="file-id" /> <input type="text" style="width:300px" name="file-ids" placeholder="file-id" />
@ -116,7 +128,7 @@ Debug Main Page
<legend>Import binfile:</legend> <legend>Import binfile:</legend>
<desc>Import penpot file in binary format.</desc> <desc>Import penpot file in binary format.</desc>
<form method="post" enctype="multipart/form-data" action="/dbg/file/import"> <form method="post" enctype="multipart/form-data" action="/dbg/actions/file-import">
<div class="row"> <div class="row">
<input type="file" name="file" value="" /> <input type="file" name="file" value="" />
</div> </div>
@ -130,79 +142,27 @@ Debug Main Page
<section class="widget"> <section class="widget">
<fieldset> <fieldset>
<legend>Reset file version</legend> <legend>Feature Flags for Team</legend>
<desc>Allows reset file data version to a specific number/</desc>
<form method="post" action="/dbg/actions/reset-file-version">
<div class="row">
<input type="text" style="width:300px" name="file-id" placeholder="file-id" />
</div>
<div class="row">
<input type="number" style="width:100px" name="version" placeholder="version" value="32" />
</div>
<div class="row">
<label for="force-version">Are you sure?</label>
<input id="force-version" type="checkbox" name="force" />
<br />
<small>
This is a just a security double check for prevent non intentional submits.
</small>
</div>
<div class="row">
<input type="submit" value="Submit" />
</div>
</form>
</fieldset>
</section>
<section class="widget">
<h2>Feature Flags</h2>
<fieldset>
<legend>Enable</legend>
<desc>Add a feature flag to a team</desc> <desc>Add a feature flag to a team</desc>
<form method="post" action="/dbg/actions/add-team-feature"> <form method="post" action="/dbg/actions/handle-team-features">
<div class="row"> <div class="row">
<input type="text" style="width:300px" name="team-id" placeholder="team-id" /> <input type="text" style="width:300px" name="team-id" placeholder="team-id" />
</div> </div>
<div class="row"> <div class="row">
<input type="text" style="width:100px" name="feature" placeholder="feature" value="" /> <select type="text" style="width:100px" name="feature">
{% for feature in supported-features %}
<option value="{{feature}}">{{feature}}</option>
{% endfor %}
</select>
</div> </div>
<div class="row"> <div class="row">
<label for="check-feature">Skip feature check</label> <select style="width:100px" name="action">
<input id="check-feature" type="checkbox" name="skip-check" /> <option value="">Action...</option>
<br /> <option value="show">Show</option>
<small> <option value="enable">Enable</option>
Do not check if the feature is supported <option value="disable">Disable</option>
</small> </select>
</div>
<div class="row">
<label for="force-version">Are you sure?</label>
<input id="force-version" type="checkbox" name="force" />
<br />
<small>
This is a just a security double check for prevent non intentional submits.
</small>
</div>
<div class="row">
<input type="submit" value="Submit" />
</div>
</form>
</fieldset>
<fieldset>
<legend>Disable</legend>
<desc>Remove a feature flag from a team</desc>
<form method="post" action="/dbg/actions/remove-team-feature">
<div class="row">
<input type="text" style="width:300px" name="team-id" placeholder="team-id" />
</div>
<div class="row">
<input type="text" style="width:100px" name="feature" placeholder="feature" value="" />
</div> </div>
<div class="row"> <div class="row">

View file

@ -7,7 +7,9 @@ penpot - error list
{% block content %} {% block content %}
<nav> <nav>
<div class="title"> <div class="title">
<h1>Error reports (last 200)</h1> <h1>Error reports (last 200)
<a href="/dbg">[GO BACK]</a>
</h1>
</div> </div>
</nav> </nav>
<main class="horizontal-list"> <main class="horizontal-list">

View file

@ -155,7 +155,7 @@
(defn decode-file (defn decode-file
"A general purpose file decoding function that resolves all external "A general purpose file decoding function that resolves all external
pointers, run migrations and return plain vanilla file map" 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)] (binding [pmap/*load-fn* (partial feat.fdata/load-pointer cfg id)]
(let [file (->> file (let [file (->> file
(feat.fmigr/resolve-applied-migrations cfg) (feat.fmigr/resolve-applied-migrations cfg)
@ -168,7 +168,7 @@
(update :data feat.fdata/process-pointers deref) (update :data feat.fdata/process-pointers deref)
(update :data feat.fdata/process-objects (partial into {})) (update :data feat.fdata/process-objects (partial into {}))
(update :data assoc :id id) (update :data assoc :id id)
(fmg/migrate-file libs))))) (cond-> migrate? (fmg/migrate-file libs))))))
(defn get-file (defn get-file
"Get file, resolve all features and apply migrations. "Get file, resolve all features and apply migrations.

View file

@ -37,3 +37,9 @@
{::db/return-keys false {::db/return-keys false
::sql/on-conflict-do-nothing true}) ::sql/on-conflict-do-nothing true})
(db/get-update-count)))) (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))

View file

@ -15,9 +15,11 @@
[app.common.features :as cfeat] [app.common.features :as cfeat]
[app.common.logging :as l] [app.common.logging :as l]
[app.common.pprint :as pp] [app.common.pprint :as pp]
[app.common.transit :as t]
[app.common.uuid :as uuid] [app.common.uuid :as uuid]
[app.config :as cf] [app.config :as cf]
[app.db :as db] [app.db :as db]
[app.features.file-migrations :as feat.fmig]
[app.http.session :as session] [app.http.session :as session]
[app.rpc.commands.auth :as auth] [app.rpc.commands.auth :as auth]
[app.rpc.commands.files-create :refer [create-file]] [app.rpc.commands.files-create :refer [create-file]]
@ -50,26 +52,26 @@
{::yres/status 200 {::yres/status 200
::yres/headers {"content-type" "text/html"} ::yres/headers {"content-type" "text/html"}
::yres/body (-> (io/resource "app/templates/debug.tmpl") ::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 ;; FILE CHANGES
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn prepare-response (defn- get-resolved-file
[body] [cfg file-id]
(let [headers {"content-type" "application/transit+json"}] (some-> (bfc/get-file cfg file-id :migrate? false)
{::yres/status 200 (update :data blob/encode)))
::yres/body body
::yres/headers headers}))
(defn prepare-download-response (defn prepare-download
[body filename] [file filename]
(let [headers {"content-disposition" (str "attachment; filename=" filename)
"content-type" "application/octet-stream"}]
{::yres/status 200 {::yres/status 200
::yres/body body ::yres/headers
::yres/headers 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 (def sql:retrieve-range-of-changes
"select revn, changes from file_change where file_id=? and revn >= ? and revn <= ? order by revn") "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 (def sql:retrieve-single-change
"select revn, changes, data from file_change where file_id=? and revn = ?") "select revn, changes, data from file_change where file_id=? and revn = ?")
(defn- retrieve-file-data (defn- download-file-data
[{:keys [::db/pool]} {:keys [params ::session/profile-id] :as request}] [cfg {:keys [params ::session/profile-id] :as request}]
(let [file-id (some-> params :file-id parse-uuid) (let [file-id (some-> params :file-id parse-uuid)
revn (some-> params :revn parse-long)
filename (str file-id)] filename (str file-id)]
(when-not file-id (when-not file-id
(ex/raise :type :validation (ex/raise :type :validation
:code :missing-arguments)) :code :missing-arguments))
(let [data (if (integer? revn) (if-let [file (get-resolved-file cfg file-id)]
(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"))
(cond (cond
(contains? params :download) (contains? params :download)
(prepare-download-response data filename) (prepare-download file filename)
(contains? params :clone) (contains? params :clone)
(let [profile (profile/get-profile pool profile-id) (db/tx-run! cfg
project-id (:default-project-id profile)] (fn [{:keys [::db/conn] :as cfg}]
(let [profile (profile/get-profile conn profile-id)
(db/run! pool (fn [{:keys [::db/conn] :as cfg}] project-id (:default-project-id profile)
(create-file cfg {:id file-id file (-> (create-file cfg {:id (uuid/next)
:name (str "Cloned file: " filename) :name (str "Cloned: " (:name file))
:features (:features file)
:project-id project-id :project-id project-id
:profile-id profile-id}) :profile-id profile-id})
(assoc :data (:data file))
(assoc :migrations (:migrations file)))]
(feat.fmig/reset-migrations! conn file)
(db/update! conn :file (db/update! conn :file
{:data data} {:data (:data file)}
{:id file-id}) {:id (:id file)}
{::db/return-keys false})
{::yres/status 201 {::yres/status 201
::yres/body "OK CREATED"}))) ::yres/body "OK CLONED"})))
:else :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? (defn- is-file-exists?
[pool id] [pool id]
@ -123,81 +131,61 @@
(-> (db/exec-one! pool [sql id]) :exists))) (-> (db/exec-one! pool [sql id]) :exists)))
(defn- upload-file-data (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) (let [profile (profile/get-profile pool profile-id)
project-id (:default-project-id profile) 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) (if (and file project-id)
(let [fname (str "Imported file *: " (dt/now)) (let [fname (str "Imported: " (:name file) "(" (dt/now) ")")
reuse-id? (contains? params :reuseid) reuse-id? (contains? params :reuseid)
file-id (or (and reuse-id? (ex/ignoring (-> params :file :filename parse-uuid))) file-id (or (and reuse-id? (ex/ignoring (-> params :file :filename parse-uuid)))
(uuid/next))] (uuid/next))]
(if (and reuse-id? file-id (if (and reuse-id? file-id
(is-file-exists? pool file-id)) (is-file-exists? pool file-id))
(do (db/tx-run! cfg
(db/update! pool :file (fn [{:keys [::db/conn] :as cfg}]
{:data data (db/update! conn :file
{:data (:data file)
:features (into-array (:features file))
:deleted-at nil} :deleted-at nil}
{:id file-id}) {:id file-id}
{::db/return-keys false})
(feat.fmig/reset-migrations! conn file)
{::yres/status 200 {::yres/status 200
::yres/body "OK UPDATED"}) ::yres/body "OK UPDATED"}))
(db/run! pool (fn [{:keys [::db/conn] :as cfg}] (db/tx-run! cfg
(create-file cfg {:id file-id (fn [{:keys [::db/conn] :as cfg}]
(let [file (-> (create-file cfg {:id file-id
:name fname :name fname
:features (:features file)
:project-id project-id :project-id project-id
:profile-id profile-id}) :profile-id profile-id})
(assoc :data (:data file))
(assoc :migrations (:migrations file)))]
(db/update! conn :file (db/update! conn :file
{:data data} {:data (:data file)}
{:id file-id}) {:id file-id}
{::db/return-keys false})
(feat.fmig/reset-migrations! conn file)
{::yres/status 201 {::yres/status 201
::yres/body "OK CREATED"})))) ::yres/body "OK CREATED"})))))
{::yres/status 500 (ex/raise :type :validation
::yres/body "ERROR"}))) :code :invalid-params
:hint "invalid file uploaded"))))
(defn file-data-handler (defn raw-export-import-handler
[cfg request] [cfg request]
(case (yreq/method request) (case (yreq/method request)
:get (retrieve-file-data cfg request) :get (download-file-data cfg request)
:post (upload-file-data cfg request) :post (upload-file-data cfg request)
(ex/raise :type :http (ex/raise :type :http
:code :method-not-found))) :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 ;; ERROR BROWSER
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@ -430,49 +418,49 @@
::yres/body "OK"})) ::yres/body "OK"}))
(defn- add-team-feature (defn- handle-team-features
[{:keys [params] :as request}] [cfg {:keys [params] :as request}]
(let [team-id (some-> params :team-id d/parse-uuid) (let [team-id (some-> params :team-id d/parse-uuid)
feature (some-> params :feature str) feature (some-> params :feature str)
action (some-> params :action)
skip-check (contains? params :skip-check)] 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) (when (nil? team-id)
(ex/raise :type :validation (ex/raise :type :validation
:code :invalid-team-id :code :invalid-team-id
:hint "provided invalid team id")) :hint "provided invalid team id"))
(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")))))})
(do
(when-not (contains? params :force)
(ex/raise :type :validation
:code :missing-force
:hint "missing force checkbox"))
(cond
(= action "enable")
(srepl/enable-team-feature! team-id feature :skip-check skip-check) (srepl/enable-team-feature! team-id feature :skip-check skip-check)
{::yres/status 200 (= action "disable")
::yres/headers {"content-type" "text/plain"}
::yres/body "OK"}))
(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)]
(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/disable-team-feature! team-id feature :skip-check skip-check) (srepl/disable-team-feature! team-id feature :skip-check skip-check)
:else
(ex/raise :type :validation
:code :invalid-action
:hint (str "invalid action: " action)))
{::yres/status 200 {::yres/status 200
::yres/headers {"content-type" "text/plain"} ::yres/headers {"content-type" "text/plain"}
::yres/body "OK"})) ::yres/body "OK"}))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; OTHER SMALL VIEWS/HANDLERS ;; OTHER SMALL VIEWS/HANDLERS
@ -525,6 +513,25 @@
(ex/raise :type :authentication (ex/raise :type :authentication
:code :only-admins-allowed)))))}) :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 (defmethod ig/assert-key ::routes
[_ params] [_ params]
(assert (db/pool? (::db/pool params)) "expected a valid database pool") (assert (db/pool? (::db/pool params)) "expected a valid database pool")
@ -540,15 +547,14 @@
["/changelog" {:handler (partial changelog-handler cfg)}] ["/changelog" {:handler (partial changelog-handler cfg)}]
["/error/:id" {:handler (partial error-handler cfg)}] ["/error/:id" {:handler (partial error-handler cfg)}]
["/error" {:handler (partial error-list-handler cfg)}] ["/error" {:handler (partial error-list-handler cfg)}]
["/actions/resend-email-verification" ["/actions" {:middleware [[errors]]}
["/resend-email-verification"
{:handler (partial resend-email-notification cfg)}] {:handler (partial resend-email-notification cfg)}]
["/actions/reset-file-version" ["/reset-file-version"
{:handler (partial reset-file-version cfg)}] {:handler (partial reset-file-version cfg)}]
["/actions/add-team-feature" ["/handle-team-features"
{:handler (partial add-team-feature)}] {:handler (partial handle-team-features cfg)}]
["/actions/remove-team-feature" ["/file-export" {:handler (partial export-handler cfg)}]
{:handler (partial remove-team-feature)}] ["/file-import" {:handler (partial import-handler cfg)}]
["/file/export" {:handler (partial export-handler cfg)}] ["/file-raw-export-import" {:handler (partial raw-export-import-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)}]]])

View file

@ -7,7 +7,6 @@
(ns app.rpc.commands.files-create (ns app.rpc.commands.files-create
(:require (:require
[app.binfile.common :as bfc] [app.binfile.common :as bfc]
[app.common.data.macros :as dm]
[app.common.features :as cfeat] [app.common.features :as cfeat]
[app.common.schema :as sm] [app.common.schema :as sm]
[app.common.types.file :as ctf] [app.common.types.file :as ctf]
@ -41,9 +40,7 @@
:or {is-shared false revn 0 create-page true} :or {is-shared false revn 0 create-page true}
:as params}] :as params}]
(dm/assert! (assert (db/connection? conn) "expected a valid connection")
"expected a valid connection"
(db/connection? conn))
(binding [pmap/*tracked* (pmap/create-tracked) (binding [pmap/*tracked* (pmap/create-tracked)
cfeat/*current* features] cfeat/*current* features]

View file

@ -78,9 +78,10 @@
(defn decode-row (defn decode-row
[{:keys [features subscription] :as row}] [{:keys [features subscription] :as row}]
(when row
(cond-> row (cond-> row
(some? features) (assoc :features (db/decode-pgarray features #{})) (some? features) (assoc :features (db/decode-pgarray features #{}))
(some? subscription) (assoc :subscription (db/decode-transit-pgobject subscription)))) (some? subscription) (assoc :subscription (db/decode-transit-pgobject subscription)))))
;; FIXME: move ;; FIXME: move
@ -461,11 +462,12 @@
;; --- COMMAND QUERY: get-team-info ;; --- COMMAND QUERY: get-team-info
(defn- get-team-info (defn get-team-info
[{:keys [::db/conn] :as cfg} {:keys [id] :as params}] [{:keys [::db/conn] :as cfg} {:keys [id] :as params}]
(db/get* conn :team (-> (db/get* conn :team
{:id id} {:id id}
{::sql/columns [:id :is-default]})) {::sql/columns [:id :is-default :features]})
(decode-row)))
(sv/defmethod ::get-team-info (sv/defmethod ::get-team-info
"Retrieve minimal team info by its ID." "Retrieve minimal team info by its ID."

View file

@ -9,17 +9,16 @@
data resources." data resources."
(:refer-clojure :exclude [read-string hash-map merge name update-vals (:refer-clojure :exclude [read-string hash-map merge name update-vals
parse-double group-by iteration concat mapcat parse-double group-by iteration concat mapcat
parse-uuid max min regexp?]) parse-uuid max min regexp? array?])
#?(:cljs #?(:cljs
(:require-macros [app.common.data])) (:require-macros [app.common.data]))
(:require (:require
#?(:cljs [cljs.core :as c]
:clj [clojure.core :as c])
#?(:cljs [cljs.reader :as r] #?(:cljs [cljs.reader :as r]
:clj [clojure.edn :as r]) :clj [clojure.edn :as r])
#?(:cljs [goog.array :as garray]) #?(:cljs [goog.array :as garray])
[app.common.math :as mth] [app.common.math :as mth]
[clojure.core :as c]
[clojure.set :as set] [clojure.set :as set]
[cuerdas.core :as str] [cuerdas.core :as str]
[linked.map :as lkm] [linked.map :as lkm]
@ -167,6 +166,15 @@
;; Data Structures Access & Manipulation ;; Data Structures Access & Manipulation
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn array?
[o]
#?(:cljs
(c/array? o)
:clj
(if (some? o)
(.isArray (class o))
false)))
(defn not-empty? (defn not-empty?
[coll] [coll]
(boolean (seq coll))) (boolean (seq coll)))